Latest commit

History

92 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

castv2

NPM versionDependency Statusnpm

An implementation of the Chromecast CASTV2 protocol

This module is an implementation of the Chromecast CASTV2 protocol over TLS. The internet is very sparse on information about the new Chromecast protocol so big props go to github.com/vincentbernat and his nodecastor module that helped me start off on the right foot and save a good deal of time in my research.

The module provides both a Client and a Server implementation of the low-level protocol. The server is (sadly) pretty useless because device authentication gets in the way for now (and maybe for good). The client still allows you to connect and exchange messages with a Chromecast dongle without any restriction.

Installation

$ npm install castv2

On windows, to avoid native modules dependencies, use

$ npm install castv2 --no-optional

Usage

varClient=require('castv2').Client;varmdns=require('mdns');varbrowser=mdns.createBrowser(mdns.tcp('googlecast'));browser.on('serviceUp',function(service){console.log('found device %s at %s:%d',service.name,service.addresses[0],service.port);ondeviceup(service.addresses[0]);browser.stop();});browser.start();functionondeviceup(host){varclient=newClient();client.connect(host,function(){// create various namespace handlersvarconnection=client.createChannel('sender-0','receiver-0','urn:x-cast:com.google.cast.tp.connection','JSON');varheartbeat=client.createChannel('sender-0','receiver-0','urn:x-cast:com.google.cast.tp.heartbeat','JSON');varreceiver=client.createChannel('sender-0','receiver-0','urn:x-cast:com.google.cast.receiver','JSON');// establish virtual connection to the receiverconnection.send({type: 'CONNECT'});// start heartbeatingsetInterval(function(){heartbeat.send({type: 'PING'});},5000);// launch YouTube appreceiver.send({type: 'LAUNCH',appId: 'YouTube',requestId: 1});// display receiver status updatesreceiver.on('message',function(data,broadcast){if(data.type='RECEIVER_STATUS'){console.log(data.status);}});});}

Run it with the following command to get a full trace of the messages exchanged with the dongle.

$ DEBUG=* node example.js

Protocol description

This is an attempt at documenting the low-level protocol. I hope it will give sender-app makers a clearer picture of what is happening behind the curtain, and give the others ideas about how this kind of protocol can be implemented. The information presented here has been collated from various internet sources (mainly example code and other attempts to implement the protocol) and my own trial and error. Correct me as needed as I may have gotten concepts or namings wrong.

The TLS / Protocol Buffers layer

The client connects to the Chromecast through TLS on port 8009. Once the connection is established server and client exchange length-prefixed binary messages (that we'll call packets).

Packets have the following structure :

+----------------+------------------------------------------------+
| Packet length | Payload (message) |
+----------------+------------------------------------------------+

Packet length is a 32 bits Big Endian Unsigned Integer (UInt32BE in nodejs parlance) that determines the payload size.

Messages are serialized with Protocol Buffers and structured as follows (excerpt of cast_channel.proto with comments stripped) :

messageCastMessage {
enumProtocolVersion {
CASTV2_1_0=0;
}
requiredProtocolVersionprotocol_version=1;
requiredstringsource_id=2;
requiredstringdestination_id=3;
requiredstringnamespace=4;
enumPayloadType {
STRING=0;
BINARY=1;
}
requiredPayloadTypepayload_type=5;
optionalstringpayload_utf8=6;
optionalbytespayload_binary=7;
}

The original .proto file can also be found in the Chromium source tree.

Using this structure the sender and receiver platforms (eg. The Chrome browser and the Chromecast device) as well as sender and receiver applications (eg. a Chromecast receiver app and a Chrome browser sender app for YouTube) communicate on channels.

Senders and receivers identify themselves through IDs : source_id and destination_id. The sending platform (eg. the Chrome browser) usually uses sender-0. The receiving platform (the Chromecast dongle) uses receiver-0. Other senders and receivers use identifiers such as sender-sdqo7ozi6s4a, client-4637 or web-4. We'll dig into that later.

Namespaces

Senders and receivers communicate through channels defined by the namespace field. Each namespace corresponds to a protocol that can have its own semantics. Protocol-specific data is carried in the payload_utf8 or payload_binary fields. Either one or the other is present in the message depending on the payload_type field value. Thanks to that, applications can define their own protocols and transparently exchange arbitrary data, including binary data, alleviating the need to establish additional connections (ie. websockets).

Though, many protocols use JSON encoded messages / commands, which makes them easy to understand and implement.

Each sender or receiver can implement one or multiple protocols. For instance the Chromecast platform (receiver-0) implements the protocols for the following namespaces : urn:x-cast:com.google.cast.tp.connection, urn:x-cast:com.google.cast.tp.heartbeat, urn:x-cast:com.google.cast.receiver and urn:x-cast:com.google.cast.tp.deviceauth.

Communicating with receivers

Before being able to exchange messages with a receiver (be it an application or the platform), a sender must establish a virtual connection with it. This is accomplished through the urn:x-cast:com.google.cast.tp.connection namespace / protocol. This has the effect of both allowing the sender to send messages to the receiver, and of subscribing the sender to the receiver's broadcasts (eg. status updates).

The protocol is JSON encoded and the semantics are pretty simple :

Message payloadDescription
{ "type": "CONNECT" }establishes a virtual connection between the sender and the receiver
{ "type": "CLOSE" }closes a virtual connection

The sender may receive a CLOSE message from the receiver that terminates the virtual connection. This sometimes happens in error cases.

Once the virtual connection is established messages can be exchanged. Broadcasts from the receiver will have a * value for the destination_id field.

Keeping the connection alive

Connections are kept alive through the urn:x-cast:com.google.cast.tp.heartbeat namespace / protocol. At regular intervals, the sender must send a PING message that will get answered by a PONG. The protocol is JSON encoded.

Message payloadDescription
{ "type": "PING" }notifies the other end that we are sill alive
{ "type": "PONG" }the other end acknowledges that we are

Failing to do so will lead to connection termination. The default interval seems to be 5 seconds. This protocol allows the Chromecast to detect unresponsive / offline senders much quicker than the TCP keepalive mechanism.

Device authentication

Device authentication enables a sender to authenticate a Chromecast device. Authenticating the device is purely optional from a sender's perspective, though the official SDK libraries do it to prevent rogue Chromecast devices to communicate with the official sender platforms. Device authentication is taken care of by the urn:x-cast:com.google.cast.tp.deviceauth namespace / protocol.

First, the sender sends a challenge message to the platform receiver receiver-0 which responds by either a response message containing a signature, certificate and a variable number of certificate authority certificates that the sent certificate is verified against or an error message. These 3 payloads are protocol buffers encoded and described in cast_channel.proto as follows :

messageAuthChallenge {
}
messageAuthResponse {
requiredbytessignature=1;
requiredbytesclient_auth_certificate=2;
repeatedbytesclient_ca=3;
}
messageAuthError {
enumErrorType {
INTERNAL_ERROR=0;
NO_TLS=1; // The underlying connection is not TLS
}
requiredErrorTypeerror_type=1;
}
messageDeviceAuthMessage {
optionalAuthChallengechallenge=1;
optionalAuthResponseresponse=2;
optionalAuthErrorerror=3;
}

The challenge message is empty in the current version of the protocol (CAST v2.1.0), yet official sender platforms are checking the returned certificate and signature. Details of the verification process can be found in this issue.

Controlling applications

The platform receiver receiver-0 implements the urn:x-cast:com.google.cast.receiver namespace / protocol which provides an interface to launch, stop, and query the status of running applications. receiver-0 also broadcast status messages on this namespace when other senders launch, stop, or affect the status of running apps. It also allows checking the app for availability.

The protocol is JSON encoded and is request / response based. Requests include a type field containing the type of the request, namely LAUNCH, STOP, GET_STATUS and GET_APP_AVAILABILITY, and a requestId field that will be reflected in the receiver's response and allows the sender to pair request and responses. requestId is not shown in the table below but must be present in every request. In the wild, it is an initially random integer that gets incremented for each subsequent request.

Message payloadDescription
{ "type": "LAUNCH", appId: <string> }launches an application
{ "type": "STOP", sessionId: <string> }stops a running instance of an application
{ "type": "GET_STATUS" }returns the status of the platform receiver, including details about running apps.
{ "type": "GET_APP_AVAILABILITY", appId: <array> }returns availability of requested apps. appId is an array of application IDs.

appId may be eg. YouTube or CC1AD845 for the Default Media Receiver app. A sessionId identifies a running instance of an application and is provided in status messages.

As these requests affect the receiver's status they all return a RECEIVER_STATUS message of the following form :

{
"requestId": 8476438,
"status": { "applications": [
{ "appId": "CC1AD845",
"displayName": "Default Media Receiver",
"namespaces": [ "urn:x-cast:com.google.cast.player.message",
"urn:x-cast:com.google.cast.media"
],
"sessionId": "7E2FF513-CDF6-9A91-2B28-3E3DE7BAC174",
"statusText": "Ready To Cast",
"transportId": "web-5" }
],
"isActiveInput": true,
"volume": { "level": 1,
"muted": false }
},
"type": "RECEIVER_STATUS"
}

This response indicates an instance of the Default Media Receiver is running with sessionId 7E2FF513-CDF6-9A91-2B28-3E3DE7BAC174. namespaces indicates which protocols are supported by the running app. This could allow any sender application implementing the media protocol to control playback on this session.

Another important field here is transportId as it is the destinationId to be used to communicate with the app. Note that the app being a receiver like any other you must issue it a CONNECT message through the urn:x-cast:com.google.cast.tp.connection protocol before being able to send messages. In this case, this will have the side effect of subscribing you to media updates (on the media channel) of this Default Media Player session.

You can join an existing session (launched by another sender) by issuing the same CONNECT message.

Controlling device volume

receiver-0 allows setting volume and muting at the device-level through the SET_VOLUME request on urn:x-cast:com.google.cast.receiver.

Message payloadDescription
{ "type": "SET_VOLUME", "volume": { level: <float> } }sets volume. level is a float between 0 and 1
{ "type": "SET_VOLUME", "volume": { muted: <boolean> } }mutes / unmutes. muted is true or false

Contributors

About

An implementation of the Chromecast CASTV2 protocol

Resources

Stars

794 stars

Watchers

53 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Latest commit

History

92 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

castv2

NPM versionDependency Statusnpm

An implementation of the Chromecast CASTV2 protocol

This module is an implementation of the Chromecast CASTV2 protocol over TLS. The internet is very sparse on information about the new Chromecast protocol so big props go to github.com/vincentbernat and his nodecastor module that helped me start off on the right foot and save a good deal of time in my research.

The module provides both a Client and a Server implementation of the low-level protocol. The server is (sadly) pretty useless because device authentication gets in the way for now (and maybe for good). The client still allows you to connect and exchange messages with a Chromecast dongle without any restriction.

Installation

$ npm install castv2

On windows, to avoid native modules dependencies, use

$ npm install castv2 --no-optional

Usage

varClient=require('castv2').Client;varmdns=require('mdns');varbrowser=mdns.createBrowser(mdns.tcp('googlecast'));browser.on('serviceUp',function(service){console.log('found device %s at %s:%d',service.name,service.addresses[0],service.port);ondeviceup(service.addresses[0]);browser.stop();});browser.start();functionondeviceup(host){varclient=newClient();client.connect(host,function(){// create various namespace handlersvarconnection=client.createChannel('sender-0','receiver-0','urn:x-cast:com.google.cast.tp.connection','JSON');varheartbeat=client.createChannel('sender-0','receiver-0','urn:x-cast:com.google.cast.tp.heartbeat','JSON');varreceiver=client.createChannel('sender-0','receiver-0','urn:x-cast:com.google.cast.receiver','JSON');// establish virtual connection to the receiverconnection.send({type: 'CONNECT'});// start heartbeatingsetInterval(function(){heartbeat.send({type: 'PING'});},5000);// launch YouTube appreceiver.send({type: 'LAUNCH',appId: 'YouTube',requestId: 1});// display receiver status updatesreceiver.on('message',function(data,broadcast){if(data.type='RECEIVER_STATUS'){console.log(data.status);}});});}

Run it with the following command to get a full trace of the messages exchanged with the dongle.

$ DEBUG=* node example.js

Protocol description

This is an attempt at documenting the low-level protocol. I hope it will give sender-app makers a clearer picture of what is happening behind the curtain, and give the others ideas about how this kind of protocol can be implemented. The information presented here has been collated from various internet sources (mainly example code and other attempts to implement the protocol) and my own trial and error. Correct me as needed as I may have gotten concepts or namings wrong.

The TLS / Protocol Buffers layer

The client connects to the Chromecast through TLS on port 8009. Once the connection is established server and client exchange length-prefixed binary messages (that we'll call packets).

Packets have the following structure :

+----------------+------------------------------------------------+
| Packet length | Payload (message) |
+----------------+------------------------------------------------+

Packet length is a 32 bits Big Endian Unsigned Integer (UInt32BE in nodejs parlance) that determines the payload size.

Messages are serialized with Protocol Buffers and structured as follows (excerpt of cast_channel.proto with comments stripped) :

messageCastMessage {
enumProtocolVersion {
CASTV2_1_0=0;
}
requiredProtocolVersionprotocol_version=1;
requiredstringsource_id=2;
requiredstringdestination_id=3;
requiredstringnamespace=4;
enumPayloadType {
STRING=0;
BINARY=1;
}
requiredPayloadTypepayload_type=5;
optionalstringpayload_utf8=6;
optionalbytespayload_binary=7;
}

The original .proto file can also be found in the Chromium source tree.

Using this structure the sender and receiver platforms (eg. The Chrome browser and the Chromecast device) as well as sender and receiver applications (eg. a Chromecast receiver app and a Chrome browser sender app for YouTube) communicate on channels.

Senders and receivers identify themselves through IDs : source_id and destination_id. The sending platform (eg. the Chrome browser) usually uses sender-0. The receiving platform (the Chromecast dongle) uses receiver-0. Other senders and receivers use identifiers such as sender-sdqo7ozi6s4a, client-4637 or web-4. We'll dig into that later.

Namespaces

Senders and receivers communicate through channels defined by the namespace field. Each namespace corresponds to a protocol that can have its own semantics. Protocol-specific data is carried in the payload_utf8 or payload_binary fields. Either one or the other is present in the message depending on the payload_type field value. Thanks to that, applications can define their own protocols and transparently exchange arbitrary data, including binary data, alleviating the need to establish additional connections (ie. websockets).

Though, many protocols use JSON encoded messages / commands, which makes them easy to understand and implement.

Each sender or receiver can implement one or multiple protocols. For instance the Chromecast platform (receiver-0) implements the protocols for the following namespaces : urn:x-cast:com.google.cast.tp.connection, urn:x-cast:com.google.cast.tp.heartbeat, urn:x-cast:com.google.cast.receiver and urn:x-cast:com.google.cast.tp.deviceauth.

Communicating with receivers

Before being able to exchange messages with a receiver (be it an application or the platform), a sender must establish a virtual connection with it. This is accomplished through the urn:x-cast:com.google.cast.tp.connection namespace / protocol. This has the effect of both allowing the sender to send messages to the receiver, and of subscribing the sender to the receiver's broadcasts (eg. status updates).

The protocol is JSON encoded and the semantics are pretty simple :

Message payloadDescription
{ "type": "CONNECT" }establishes a virtual connection between the sender and the receiver
{ "type": "CLOSE" }closes a virtual connection

The sender may receive a CLOSE message from the receiver that terminates the virtual connection. This sometimes happens in error cases.

Once the virtual connection is established messages can be exchanged. Broadcasts from the receiver will have a * value for the destination_id field.

Keeping the connection alive

Connections are kept alive through the urn:x-cast:com.google.cast.tp.heartbeat namespace / protocol. At regular intervals, the sender must send a PING message that will get answered by a PONG. The protocol is JSON encoded.

Message payloadDescription
{ "type": "PING" }notifies the other end that we are sill alive
{ "type": "PONG" }the other end acknowledges that we are

Failing to do so will lead to connection termination. The default interval seems to be 5 seconds. This protocol allows the Chromecast to detect unresponsive / offline senders much quicker than the TCP keepalive mechanism.

Device authentication

Device authentication enables a sender to authenticate a Chromecast device. Authenticating the device is purely optional from a sender's perspective, though the official SDK libraries do it to prevent rogue Chromecast devices to communicate with the official sender platforms. Device authentication is taken care of by the urn:x-cast:com.google.cast.tp.deviceauth namespace / protocol.

First, the sender sends a challenge message to the platform receiver receiver-0 which responds by either a response message containing a signature, certificate and a variable number of certificate authority certificates that the sent certificate is verified against or an error message. These 3 payloads are protocol buffers encoded and described in cast_channel.proto as follows :

messageAuthChallenge {
}
messageAuthResponse {
requiredbytessignature=1;
requiredbytesclient_auth_certificate=2;
repeatedbytesclient_ca=3;
}
messageAuthError {
enumErrorType {
INTERNAL_ERROR=0;
NO_TLS=1; // The underlying connection is not TLS
}
requiredErrorTypeerror_type=1;
}
messageDeviceAuthMessage {
optionalAuthChallengechallenge=1;
optionalAuthResponseresponse=2;
optionalAuthErrorerror=3;
}

The challenge message is empty in the current version of the protocol (CAST v2.1.0), yet official sender platforms are checking the returned certificate and signature. Details of the verification process can be found in this issue.

Controlling applications

The platform receiver receiver-0 implements the urn:x-cast:com.google.cast.receiver namespace / protocol which provides an interface to launch, stop, and query the status of running applications. receiver-0 also broadcast status messages on this namespace when other senders launch, stop, or affect the status of running apps. It also allows checking the app for availability.

The protocol is JSON encoded and is request / response based. Requests include a type field containing the type of the request, namely LAUNCH, STOP, GET_STATUS and GET_APP_AVAILABILITY, and a requestId field that will be reflected in the receiver's response and allows the sender to pair request and responses. requestId is not shown in the table below but must be present in every request. In the wild, it is an initially random integer that gets incremented for each subsequent request.

Message payloadDescription
{ "type": "LAUNCH", appId: <string> }launches an application
{ "type": "STOP", sessionId: <string> }stops a running instance of an application
{ "type": "GET_STATUS" }returns the status of the platform receiver, including details about running apps.
{ "type": "GET_APP_AVAILABILITY", appId: <array> }returns availability of requested apps. appId is an array of application IDs.

appId may be eg. YouTube or CC1AD845 for the Default Media Receiver app. A sessionId identifies a running instance of an application and is provided in status messages.

As these requests affect the receiver's status they all return a RECEIVER_STATUS message of the following form :

{
"requestId": 8476438,
"status": { "applications": [
{ "appId": "CC1AD845",
"displayName": "Default Media Receiver",
"namespaces": [ "urn:x-cast:com.google.cast.player.message",
"urn:x-cast:com.google.cast.media"
],
"sessionId": "7E2FF513-CDF6-9A91-2B28-3E3DE7BAC174",
"statusText": "Ready To Cast",
"transportId": "web-5" }
],
"isActiveInput": true,
"volume": { "level": 1,
"muted": false }
},
"type": "RECEIVER_STATUS"
}

This response indicates an instance of the Default Media Receiver is running with sessionId 7E2FF513-CDF6-9A91-2B28-3E3DE7BAC174. namespaces indicates which protocols are supported by the running app. This could allow any sender application implementing the media protocol to control playback on this session.

Another important field here is transportId as it is the destinationId to be used to communicate with the app. Note that the app being a receiver like any other you must issue it a CONNECT message through the urn:x-cast:com.google.cast.tp.connection protocol before being able to send messages. In this case, this will have the side effect of subscribing you to media updates (on the media channel) of this Default Media Player session.

You can join an existing session (launched by another sender) by issuing the same CONNECT message.

Controlling device volume

receiver-0 allows setting volume and muting at the device-level through the SET_VOLUME request on urn:x-cast:com.google.cast.receiver.

Message payloadDescription
{ "type": "SET_VOLUME", "volume": { level: <float> } }sets volume. level is a float between 0 and 1
{ "type": "SET_VOLUME", "volume": { muted: <boolean> } }mutes / unmutes. muted is true or false

Contributors

About

An implementation of the Chromecast CASTV2 protocol

Resources

Stars

794 stars

Watchers

53 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

92 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

castv2

NPM versionDependency Statusnpm

An implementation of the Chromecast CASTV2 protocol

This module is an implementation of the Chromecast CASTV2 protocol over TLS. The internet is very sparse on information about the new Chromecast protocol so big props go to github.com/vincentbernat and his nodecastor module that helped me start off on the right foot and save a good deal of time in my research.

The module provides both a Client and a Server implementation of the low-level protocol. The server is (sadly) pretty useless because device authentication gets in the way for now (and maybe for good). The client still allows you to connect and exchange messages with a Chromecast dongle without any restriction.

Installation

$ npm install castv2

On windows, to avoid native modules dependencies, use

$ npm install castv2 --no-optional

Usage

varClient=require('castv2').Client;varmdns=require('mdns');varbrowser=mdns.createBrowser(mdns.tcp('googlecast'));browser.on('serviceUp',function(service){console.log('found device %s at %s:%d',service.name,service.addresses[0],service.port);ondeviceup(service.addresses[0]);browser.stop();});browser.start();functionondeviceup(host){varclient=newClient();client.connect(host,function(){// create various namespace handlersvarconnection=client.createChannel('sender-0','receiver-0','urn:x-cast:com.google.cast.tp.connection','JSON');varheartbeat=client.createChannel('sender-0','receiver-0','urn:x-cast:com.google.cast.tp.heartbeat','JSON');varreceiver=client.createChannel('sender-0','receiver-0','urn:x-cast:com.google.cast.receiver','JSON');// establish virtual connection to the receiverconnection.send({type: 'CONNECT'});// start heartbeatingsetInterval(function(){heartbeat.send({type: 'PING'});},5000);// launch YouTube appreceiver.send({type: 'LAUNCH',appId: 'YouTube',requestId: 1});// display receiver status updatesreceiver.on('message',function(data,broadcast){if(data.type='RECEIVER_STATUS'){console.log(data.status);}});});}

Run it with the following command to get a full trace of the messages exchanged with the dongle.

$ DEBUG=* node example.js

Protocol description

This is an attempt at documenting the low-level protocol. I hope it will give sender-app makers a clearer picture of what is happening behind the curtain, and give the others ideas about how this kind of protocol can be implemented. The information presented here has been collated from various internet sources (mainly example code and other attempts to implement the protocol) and my own trial and error. Correct me as needed as I may have gotten concepts or namings wrong.

The TLS / Protocol Buffers layer

The client connects to the Chromecast through TLS on port 8009. Once the connection is established server and client exchange length-prefixed binary messages (that we'll call packets).

Packets have the following structure :

+----------------+------------------------------------------------+
| Packet length | Payload (message) |
+----------------+------------------------------------------------+

Packet length is a 32 bits Big Endian Unsigned Integer (UInt32BE in nodejs parlance) that determines the payload size.

Messages are serialized with Protocol Buffers and structured as follows (excerpt of cast_channel.proto with comments stripped) :

messageCastMessage {
enumProtocolVersion {
CASTV2_1_0=0;
}
requiredProtocolVersionprotocol_version=1;
requiredstringsource_id=2;
requiredstringdestination_id=3;
requiredstringnamespace=4;
enumPayloadType {
STRING=0;
BINARY=1;
}
requiredPayloadTypepayload_type=5;
optionalstringpayload_utf8=6;
optionalbytespayload_binary=7;
}

The original .proto file can also be found in the Chromium source tree.

Using this structure the sender and receiver platforms (eg. The Chrome browser and the Chromecast device) as well as sender and receiver applications (eg. a Chromecast receiver app and a Chrome browser sender app for YouTube) communicate on channels.

Senders and receivers identify themselves through IDs : source_id and destination_id. The sending platform (eg. the Chrome browser) usually uses sender-0. The receiving platform (the Chromecast dongle) uses receiver-0. Other senders and receivers use identifiers such as sender-sdqo7ozi6s4a, client-4637 or web-4. We'll dig into that later.

Namespaces

Senders and receivers communicate through channels defined by the namespace field. Each namespace corresponds to a protocol that can have its own semantics. Protocol-specific data is carried in the payload_utf8 or payload_binary fields. Either one or the other is present in the message depending on the payload_type field value. Thanks to that, applications can define their own protocols and transparently exchange arbitrary data, including binary data, alleviating the need to establish additional connections (ie. websockets).

Though, many protocols use JSON encoded messages / commands, which makes them easy to understand and implement.

Each sender or receiver can implement one or multiple protocols. For instance the Chromecast platform (receiver-0) implements the protocols for the following namespaces : urn:x-cast:com.google.cast.tp.connection, urn:x-cast:com.google.cast.tp.heartbeat, urn:x-cast:com.google.cast.receiver and urn:x-cast:com.google.cast.tp.deviceauth.

Communicating with receivers

Before being able to exchange messages with a receiver (be it an application or the platform), a sender must establish a virtual connection with it. This is accomplished through the urn:x-cast:com.google.cast.tp.connection namespace / protocol. This has the effect of both allowing the sender to send messages to the receiver, and of subscribing the sender to the receiver's broadcasts (eg. status updates).

The protocol is JSON encoded and the semantics are pretty simple :

Message payloadDescription
{ "type": "CONNECT" }establishes a virtual connection between the sender and the receiver
{ "type": "CLOSE" }closes a virtual connection

The sender may receive a CLOSE message from the receiver that terminates the virtual connection. This sometimes happens in error cases.

Once the virtual connection is established messages can be exchanged. Broadcasts from the receiver will have a * value for the destination_id field.

Keeping the connection alive

Connections are kept alive through the urn:x-cast:com.google.cast.tp.heartbeat namespace / protocol. At regular intervals, the sender must send a PING message that will get answered by a PONG. The protocol is JSON encoded.

Message payloadDescription
{ "type": "PING" }notifies the other end that we are sill alive
{ "type": "PONG" }the other end acknowledges that we are

Failing to do so will lead to connection termination. The default interval seems to be 5 seconds. This protocol allows the Chromecast to detect unresponsive / offline senders much quicker than the TCP keepalive mechanism.

Device authentication

Device authentication enables a sender to authenticate a Chromecast device. Authenticating the device is purely optional from a sender's perspective, though the official SDK libraries do it to prevent rogue Chromecast devices to communicate with the official sender platforms. Device authentication is taken care of by the urn:x-cast:com.google.cast.tp.deviceauth namespace / protocol.

First, the sender sends a challenge message to the platform receiver receiver-0 which responds by either a response message containing a signature, certificate and a variable number of certificate authority certificates that the sent certificate is verified against or an error message. These 3 payloads are protocol buffers encoded and described in cast_channel.proto as follows :

messageAuthChallenge {
}
messageAuthResponse {
requiredbytessignature=1;
requiredbytesclient_auth_certificate=2;
repeatedbytesclient_ca=3;
}
messageAuthError {
enumErrorType {
INTERNAL_ERROR=0;
NO_TLS=1; // The underlying connection is not TLS
}
requiredErrorTypeerror_type=1;
}
messageDeviceAuthMessage {
optionalAuthChallengechallenge=1;
optionalAuthResponseresponse=2;
optionalAuthErrorerror=3;
}

The challenge message is empty in the current version of the protocol (CAST v2.1.0), yet official sender platforms are checking the returned certificate and signature. Details of the verification process can be found in this issue.

Controlling applications

The platform receiver receiver-0 implements the urn:x-cast:com.google.cast.receiver namespace / protocol which provides an interface to launch, stop, and query the status of running applications. receiver-0 also broadcast status messages on this namespace when other senders launch, stop, or affect the status of running apps. It also allows checking the app for availability.

The protocol is JSON encoded and is request / response based. Requests include a type field containing the type of the request, namely LAUNCH, STOP, GET_STATUS and GET_APP_AVAILABILITY, and a requestId field that will be reflected in the receiver's response and allows the sender to pair request and responses. requestId is not shown in the table below but must be present in every request. In the wild, it is an initially random integer that gets incremented for each subsequent request.

Message payloadDescription
{ "type": "LAUNCH", appId: <string> }launches an application
{ "type": "STOP", sessionId: <string> }stops a running instance of an application
{ "type": "GET_STATUS" }returns the status of the platform receiver, including details about running apps.
{ "type": "GET_APP_AVAILABILITY", appId: <array> }returns availability of requested apps. appId is an array of application IDs.

appId may be eg. YouTube or CC1AD845 for the Default Media Receiver app. A sessionId identifies a running instance of an application and is provided in status messages.

As these requests affect the receiver's status they all return a RECEIVER_STATUS message of the following form :

{
"requestId": 8476438,
"status": { "applications": [
{ "appId": "CC1AD845",
"displayName": "Default Media Receiver",
"namespaces": [ "urn:x-cast:com.google.cast.player.message",
"urn:x-cast:com.google.cast.media"
],
"sessionId": "7E2FF513-CDF6-9A91-2B28-3E3DE7BAC174",
"statusText": "Ready To Cast",
"transportId": "web-5" }
],
"isActiveInput": true,
"volume": { "level": 1,
"muted": false }
},
"type": "RECEIVER_STATUS"
}

This response indicates an instance of the Default Media Receiver is running with sessionId 7E2FF513-CDF6-9A91-2B28-3E3DE7BAC174. namespaces indicates which protocols are supported by the running app. This could allow any sender application implementing the media protocol to control playback on this session.

Another important field here is transportId as it is the destinationId to be used to communicate with the app. Note that the app being a receiver like any other you must issue it a CONNECT message through the urn:x-cast:com.google.cast.tp.connection protocol before being able to send messages. In this case, this will have the side effect of subscribing you to media updates (on the media channel) of this Default Media Player session.

You can join an existing session (launched by another sender) by issuing the same CONNECT message.

Controlling device volume

receiver-0 allows setting volume and muting at the device-level through the SET_VOLUME request on urn:x-cast:com.google.cast.receiver.

Message payloadDescription
{ "type": "SET_VOLUME", "volume": { level: <float> } }sets volume. level is a float between 0 and 1
{ "type": "SET_VOLUME", "volume": { muted: <boolean> } }mutes / unmutes. muted is true or false

Contributors

About

An implementation of the Chromecast CASTV2 protocol

Resources

Stars

794 stars

Watchers

53 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

92 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

castv2

NPM versionDependency Statusnpm

An implementation of the Chromecast CASTV2 protocol

This module is an implementation of the Chromecast CASTV2 protocol over TLS. The internet is very sparse on information about the new Chromecast protocol so big props go to github.com/vincentbernat and his nodecastor module that helped me start off on the right foot and save a good deal of time in my research.

The module provides both a Client and a Server implementation of the low-level protocol. The server is (sadly) pretty useless because device authentication gets in the way for now (and maybe for good). The client still allows you to connect and exchange messages with a Chromecast dongle without any restriction.

Installation

$ npm install castv2

On windows, to avoid native modules dependencies, use

$ npm install castv2 --no-optional

Usage

varClient=require('castv2').Client;varmdns=require('mdns');varbrowser=mdns.createBrowser(mdns.tcp('googlecast'));browser.on('serviceUp',function(service){console.log('found device %s at %s:%d',service.name,service.addresses[0],service.port);ondeviceup(service.addresses[0]);browser.stop();});browser.start();functionondeviceup(host){varclient=newClient();client.connect(host,function(){// create various namespace handlersvarconnection=client.createChannel('sender-0','receiver-0','urn:x-cast:com.google.cast.tp.connection','JSON');varheartbeat=client.createChannel('sender-0','receiver-0','urn:x-cast:com.google.cast.tp.heartbeat','JSON');varreceiver=client.createChannel('sender-0','receiver-0','urn:x-cast:com.google.cast.receiver','JSON');// establish virtual connection to the receiverconnection.send({type: 'CONNECT'});// start heartbeatingsetInterval(function(){heartbeat.send({type: 'PING'});},5000);// launch YouTube appreceiver.send({type: 'LAUNCH',appId: 'YouTube',requestId: 1});// display receiver status updatesreceiver.on('message',function(data,broadcast){if(data.type='RECEIVER_STATUS'){console.log(data.status);}});});}

Run it with the following command to get a full trace of the messages exchanged with the dongle.

$ DEBUG=* node example.js

Protocol description

This is an attempt at documenting the low-level protocol. I hope it will give sender-app makers a clearer picture of what is happening behind the curtain, and give the others ideas about how this kind of protocol can be implemented. The information presented here has been collated from various internet sources (mainly example code and other attempts to implement the protocol) and my own trial and error. Correct me as needed as I may have gotten concepts or namings wrong.

The TLS / Protocol Buffers layer

The client connects to the Chromecast through TLS on port 8009. Once the connection is established server and client exchange length-prefixed binary messages (that we'll call packets).

Packets have the following structure :

+----------------+------------------------------------------------+
| Packet length | Payload (message) |
+----------------+------------------------------------------------+

Packet length is a 32 bits Big Endian Unsigned Integer (UInt32BE in nodejs parlance) that determines the payload size.

Messages are serialized with Protocol Buffers and structured as follows (excerpt of cast_channel.proto with comments stripped) :

messageCastMessage {
enumProtocolVersion {
CASTV2_1_0=0;
}
requiredProtocolVersionprotocol_version=1;
requiredstringsource_id=2;
requiredstringdestination_id=3;
requiredstringnamespace=4;
enumPayloadType {
STRING=0;
BINARY=1;
}
requiredPayloadTypepayload_type=5;
optionalstringpayload_utf8=6;
optionalbytespayload_binary=7;
}

The original .proto file can also be found in the Chromium source tree.

Using this structure the sender and receiver platforms (eg. The Chrome browser and the Chromecast device) as well as sender and receiver applications (eg. a Chromecast receiver app and a Chrome browser sender app for YouTube) communicate on channels.

Senders and receivers identify themselves through IDs : source_id and destination_id. The sending platform (eg. the Chrome browser) usually uses sender-0. The receiving platform (the Chromecast dongle) uses receiver-0. Other senders and receivers use identifiers such as sender-sdqo7ozi6s4a, client-4637 or web-4. We'll dig into that later.

Namespaces

Senders and receivers communicate through channels defined by the namespace field. Each namespace corresponds to a protocol that can have its own semantics. Protocol-specific data is carried in the payload_utf8 or payload_binary fields. Either one or the other is present in the message depending on the payload_type field value. Thanks to that, applications can define their own protocols and transparently exchange arbitrary data, including binary data, alleviating the need to establish additional connections (ie. websockets).

Though, many protocols use JSON encoded messages / commands, which makes them easy to understand and implement.

Each sender or receiver can implement one or multiple protocols. For instance the Chromecast platform (receiver-0) implements the protocols for the following namespaces : urn:x-cast:com.google.cast.tp.connection, urn:x-cast:com.google.cast.tp.heartbeat, urn:x-cast:com.google.cast.receiver and urn:x-cast:com.google.cast.tp.deviceauth.

Communicating with receivers

Before being able to exchange messages with a receiver (be it an application or the platform), a sender must establish a virtual connection with it. This is accomplished through the urn:x-cast:com.google.cast.tp.connection namespace / protocol. This has the effect of both allowing the sender to send messages to the receiver, and of subscribing the sender to the receiver's broadcasts (eg. status updates).

The protocol is JSON encoded and the semantics are pretty simple :

Message payloadDescription
{ "type": "CONNECT" }establishes a virtual connection between the sender and the receiver
{ "type": "CLOSE" }closes a virtual connection

The sender may receive a CLOSE message from the receiver that terminates the virtual connection. This sometimes happens in error cases.

Once the virtual connection is established messages can be exchanged. Broadcasts from the receiver will have a * value for the destination_id field.

Keeping the connection alive

Connections are kept alive through the urn:x-cast:com.google.cast.tp.heartbeat namespace / protocol. At regular intervals, the sender must send a PING message that will get answered by a PONG. The protocol is JSON encoded.

Message payloadDescription
{ "type": "PING" }notifies the other end that we are sill alive
{ "type": "PONG" }the other end acknowledges that we are

Failing to do so will lead to connection termination. The default interval seems to be 5 seconds. This protocol allows the Chromecast to detect unresponsive / offline senders much quicker than the TCP keepalive mechanism.

Device authentication

Device authentication enables a sender to authenticate a Chromecast device. Authenticating the device is purely optional from a sender's perspective, though the official SDK libraries do it to prevent rogue Chromecast devices to communicate with the official sender platforms. Device authentication is taken care of by the urn:x-cast:com.google.cast.tp.deviceauth namespace / protocol.

First, the sender sends a challenge message to the platform receiver receiver-0 which responds by either a response message containing a signature, certificate and a variable number of certificate authority certificates that the sent certificate is verified against or an error message. These 3 payloads are protocol buffers encoded and described in cast_channel.proto as follows :

messageAuthChallenge {
}
messageAuthResponse {
requiredbytessignature=1;
requiredbytesclient_auth_certificate=2;
repeatedbytesclient_ca=3;
}
messageAuthError {
enumErrorType {
INTERNAL_ERROR=0;
NO_TLS=1; // The underlying connection is not TLS
}
requiredErrorTypeerror_type=1;
}
messageDeviceAuthMessage {
optionalAuthChallengechallenge=1;
optionalAuthResponseresponse=2;
optionalAuthErrorerror=3;
}

The challenge message is empty in the current version of the protocol (CAST v2.1.0), yet official sender platforms are checking the returned certificate and signature. Details of the verification process can be found in this issue.

Controlling applications

The platform receiver receiver-0 implements the urn:x-cast:com.google.cast.receiver namespace / protocol which provides an interface to launch, stop, and query the status of running applications. receiver-0 also broadcast status messages on this namespace when other senders launch, stop, or affect the status of running apps. It also allows checking the app for availability.

The protocol is JSON encoded and is request / response based. Requests include a type field containing the type of the request, namely LAUNCH, STOP, GET_STATUS and GET_APP_AVAILABILITY, and a requestId field that will be reflected in the receiver's response and allows the sender to pair request and responses. requestId is not shown in the table below but must be present in every request. In the wild, it is an initially random integer that gets incremented for each subsequent request.

Message payloadDescription
{ "type": "LAUNCH", appId: <string> }launches an application
{ "type": "STOP", sessionId: <string> }stops a running instance of an application
{ "type": "GET_STATUS" }returns the status of the platform receiver, including details about running apps.
{ "type": "GET_APP_AVAILABILITY", appId: <array> }returns availability of requested apps. appId is an array of application IDs.

appId may be eg. YouTube or CC1AD845 for the Default Media Receiver app. A sessionId identifies a running instance of an application and is provided in status messages.

As these requests affect the receiver's status they all return a RECEIVER_STATUS message of the following form :

{
"requestId": 8476438,
"status": { "applications": [
{ "appId": "CC1AD845",
"displayName": "Default Media Receiver",
"namespaces": [ "urn:x-cast:com.google.cast.player.message",
"urn:x-cast:com.google.cast.media"
],
"sessionId": "7E2FF513-CDF6-9A91-2B28-3E3DE7BAC174",
"statusText": "Ready To Cast",
"transportId": "web-5" }
],
"isActiveInput": true,
"volume": { "level": 1,
"muted": false }
},
"type": "RECEIVER_STATUS"
}

This response indicates an instance of the Default Media Receiver is running with sessionId 7E2FF513-CDF6-9A91-2B28-3E3DE7BAC174. namespaces indicates which protocols are supported by the running app. This could allow any sender application implementing the media protocol to control playback on this session.

Another important field here is transportId as it is the destinationId to be used to communicate with the app. Note that the app being a receiver like any other you must issue it a CONNECT message through the urn:x-cast:com.google.cast.tp.connection protocol before being able to send messages. In this case, this will have the side effect of subscribing you to media updates (on the media channel) of this Default Media Player session.

You can join an existing session (launched by another sender) by issuing the same CONNECT message.

Controlling device volume

receiver-0 allows setting volume and muting at the device-level through the SET_VOLUME request on urn:x-cast:com.google.cast.receiver.

Message payloadDescription
{ "type": "SET_VOLUME", "volume": { level: <float> } }sets volume. level is a float between 0 and 1
{ "type": "SET_VOLUME", "volume": { muted: <boolean> } }mutes / unmutes. muted is true or false

Contributors

About

An implementation of the Chromecast CASTV2 protocol

Resources

Stars

794 stars

Watchers

53 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Latest commit

History

92 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

castv2

NPM versionDependency Statusnpm

An implementation of the Chromecast CASTV2 protocol

This module is an implementation of the Chromecast CASTV2 protocol over TLS. The internet is very sparse on information about the new Chromecast protocol so big props go to github.com/vincentbernat and his nodecastor module that helped me start off on the right foot and save a good deal of time in my research.

The module provides both a Client and a Server implementation of the low-level protocol. The server is (sadly) pretty useless because device authentication gets in the way for now (and maybe for good). The client still allows you to connect and exchange messages with a Chromecast dongle without any restriction.

Installation

$ npm install castv2

On windows, to avoid native modules dependencies, use

$ npm install castv2 --no-optional

Usage

varClient=require('castv2').Client;varmdns=require('mdns');varbrowser=mdns.createBrowser(mdns.tcp('googlecast'));browser.on('serviceUp',function(service){console.log('found device %s at %s:%d',service.name,service.addresses[0],service.port);ondeviceup(service.addresses[0]);browser.stop();});browser.start();functionondeviceup(host){varclient=newClient();client.connect(host,function(){// create various namespace handlersvarconnection=client.createChannel('sender-0','receiver-0','urn:x-cast:com.google.cast.tp.connection','JSON');varheartbeat=client.createChannel('sender-0','receiver-0','urn:x-cast:com.google.cast.tp.heartbeat','JSON');varreceiver=client.createChannel('sender-0','receiver-0','urn:x-cast:com.google.cast.receiver','JSON');// establish virtual connection to the receiverconnection.send({type: 'CONNECT'});// start heartbeatingsetInterval(function(){heartbeat.send({type: 'PING'});},5000);// launch YouTube appreceiver.send({type: 'LAUNCH',appId: 'YouTube',requestId: 1});// display receiver status updatesreceiver.on('message',function(data,broadcast){if(data.type='RECEIVER_STATUS'){console.log(data.status);}});});}

Run it with the following command to get a full trace of the messages exchanged with the dongle.

$ DEBUG=* node example.js

Protocol description

This is an attempt at documenting the low-level protocol. I hope it will give sender-app makers a clearer picture of what is happening behind the curtain, and give the others ideas about how this kind of protocol can be implemented. The information presented here has been collated from various internet sources (mainly example code and other attempts to implement the protocol) and my own trial and error. Correct me as needed as I may have gotten concepts or namings wrong.

The TLS / Protocol Buffers layer

The client connects to the Chromecast through TLS on port 8009. Once the connection is established server and client exchange length-prefixed binary messages (that we'll call packets).

Packets have the following structure :

+----------------+------------------------------------------------+
| Packet length | Payload (message) |
+----------------+------------------------------------------------+

Packet length is a 32 bits Big Endian Unsigned Integer (UInt32BE in nodejs parlance) that determines the payload size.

Messages are serialized with Protocol Buffers and structured as follows (excerpt of cast_channel.proto with comments stripped) :

messageCastMessage {
enumProtocolVersion {
CASTV2_1_0=0;
}
requiredProtocolVersionprotocol_version=1;
requiredstringsource_id=2;
requiredstringdestination_id=3;
requiredstringnamespace=4;
enumPayloadType {
STRING=0;
BINARY=1;
}
requiredPayloadTypepayload_type=5;
optionalstringpayload_utf8=6;
optionalbytespayload_binary=7;
}

The original .proto file can also be found in the Chromium source tree.

Using this structure the sender and receiver platforms (eg. The Chrome browser and the Chromecast device) as well as sender and receiver applications (eg. a Chromecast receiver app and a Chrome browser sender app for YouTube) communicate on channels.

Senders and receivers identify themselves through IDs : source_id and destination_id. The sending platform (eg. the Chrome browser) usually uses sender-0. The receiving platform (the Chromecast dongle) uses receiver-0. Other senders and receivers use identifiers such as sender-sdqo7ozi6s4a, client-4637 or web-4. We'll dig into that later.

Namespaces

Senders and receivers communicate through channels defined by the namespace field. Each namespace corresponds to a protocol that can have its own semantics. Protocol-specific data is carried in the payload_utf8 or payload_binary fields. Either one or the other is present in the message depending on the payload_type field value. Thanks to that, applications can define their own protocols and transparently exchange arbitrary data, including binary data, alleviating the need to establish additional connections (ie. websockets).

Though, many protocols use JSON encoded messages / commands, which makes them easy to understand and implement.

Each sender or receiver can implement one or multiple protocols. For instance the Chromecast platform (receiver-0) implements the protocols for the following namespaces : urn:x-cast:com.google.cast.tp.connection, urn:x-cast:com.google.cast.tp.heartbeat, urn:x-cast:com.google.cast.receiver and urn:x-cast:com.google.cast.tp.deviceauth.

Communicating with receivers

Before being able to exchange messages with a receiver (be it an application or the platform), a sender must establish a virtual connection with it. This is accomplished through the urn:x-cast:com.google.cast.tp.connection namespace / protocol. This has the effect of both allowing the sender to send messages to the receiver, and of subscribing the sender to the receiver's broadcasts (eg. status updates).

The protocol is JSON encoded and the semantics are pretty simple :

Message payloadDescription
{ "type": "CONNECT" }establishes a virtual connection between the sender and the receiver
{ "type": "CLOSE" }closes a virtual connection

The sender may receive a CLOSE message from the receiver that terminates the virtual connection. This sometimes happens in error cases.

Once the virtual connection is established messages can be exchanged. Broadcasts from the receiver will have a * value for the destination_id field.

Keeping the connection alive

Connections are kept alive through the urn:x-cast:com.google.cast.tp.heartbeat namespace / protocol. At regular intervals, the sender must send a PING message that will get answered by a PONG. The protocol is JSON encoded.

Message payloadDescription
{ "type": "PING" }notifies the other end that we are sill alive
{ "type": "PONG" }the other end acknowledges that we are

Failing to do so will lead to connection termination. The default interval seems to be 5 seconds. This protocol allows the Chromecast to detect unresponsive / offline senders much quicker than the TCP keepalive mechanism.

Device authentication

Device authentication enables a sender to authenticate a Chromecast device. Authenticating the device is purely optional from a sender's perspective, though the official SDK libraries do it to prevent rogue Chromecast devices to communicate with the official sender platforms. Device authentication is taken care of by the urn:x-cast:com.google.cast.tp.deviceauth namespace / protocol.

First, the sender sends a challenge message to the platform receiver receiver-0 which responds by either a response message containing a signature, certificate and a variable number of certificate authority certificates that the sent certificate is verified against or an error message. These 3 payloads are protocol buffers encoded and described in cast_channel.proto as follows :

messageAuthChallenge {
}
messageAuthResponse {
requiredbytessignature=1;
requiredbytesclient_auth_certificate=2;
repeatedbytesclient_ca=3;
}
messageAuthError {
enumErrorType {
INTERNAL_ERROR=0;
NO_TLS=1; // The underlying connection is not TLS
}
requiredErrorTypeerror_type=1;
}
messageDeviceAuthMessage {
optionalAuthChallengechallenge=1;
optionalAuthResponseresponse=2;
optionalAuthErrorerror=3;
}

The challenge message is empty in the current version of the protocol (CAST v2.1.0), yet official sender platforms are checking the returned certificate and signature. Details of the verification process can be found in this issue.

Controlling applications

The platform receiver receiver-0 implements the urn:x-cast:com.google.cast.receiver namespace / protocol which provides an interface to launch, stop, and query the status of running applications. receiver-0 also broadcast status messages on this namespace when other senders launch, stop, or affect the status of running apps. It also allows checking the app for availability.

The protocol is JSON encoded and is request / response based. Requests include a type field containing the type of the request, namely LAUNCH, STOP, GET_STATUS and GET_APP_AVAILABILITY, and a requestId field that will be reflected in the receiver's response and allows the sender to pair request and responses. requestId is not shown in the table below but must be present in every request. In the wild, it is an initially random integer that gets incremented for each subsequent request.

Message payloadDescription
{ "type": "LAUNCH", appId: <string> }launches an application
{ "type": "STOP", sessionId: <string> }stops a running instance of an application
{ "type": "GET_STATUS" }returns the status of the platform receiver, including details about running apps.
{ "type": "GET_APP_AVAILABILITY", appId: <array> }returns availability of requested apps. appId is an array of application IDs.

appId may be eg. YouTube or CC1AD845 for the Default Media Receiver app. A sessionId identifies a running instance of an application and is provided in status messages.

As these requests affect the receiver's status they all return a RECEIVER_STATUS message of the following form :

{
"requestId": 8476438,
"status": { "applications": [
{ "appId": "CC1AD845",
"displayName": "Default Media Receiver",
"namespaces": [ "urn:x-cast:com.google.cast.player.message",
"urn:x-cast:com.google.cast.media"
],
"sessionId": "7E2FF513-CDF6-9A91-2B28-3E3DE7BAC174",
"statusText": "Ready To Cast",
"transportId": "web-5" }
],
"isActiveInput": true,
"volume": { "level": 1,
"muted": false }
},
"type": "RECEIVER_STATUS"
}

This response indicates an instance of the Default Media Receiver is running with sessionId 7E2FF513-CDF6-9A91-2B28-3E3DE7BAC174. namespaces indicates which protocols are supported by the running app. This could allow any sender application implementing the media protocol to control playback on this session.

Another important field here is transportId as it is the destinationId to be used to communicate with the app. Note that the app being a receiver like any other you must issue it a CONNECT message through the urn:x-cast:com.google.cast.tp.connection protocol before being able to send messages. In this case, this will have the side effect of subscribing you to media updates (on the media channel) of this Default Media Player session.

You can join an existing session (launched by another sender) by issuing the same CONNECT message.

Controlling device volume

receiver-0 allows setting volume and muting at the device-level through the SET_VOLUME request on urn:x-cast:com.google.cast.receiver.

Message payloadDescription
{ "type": "SET_VOLUME", "volume": { level: <float> } }sets volume. level is a float between 0 and 1
{ "type": "SET_VOLUME", "volume": { muted: <boolean> } }mutes / unmutes. muted is true or false

Contributors

About

An implementation of the Chromecast CASTV2 protocol

Resources

Stars

794 stars

Watchers

53 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

92 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

castv2

NPM versionDependency Statusnpm

An implementation of the Chromecast CASTV2 protocol

This module is an implementation of the Chromecast CASTV2 protocol over TLS. The internet is very sparse on information about the new Chromecast protocol so big props go to github.com/vincentbernat and his nodecastor module that helped me start off on the right foot and save a good deal of time in my research.

The module provides both a Client and a Server implementation of the low-level protocol. The server is (sadly) pretty useless because device authentication gets in the way for now (and maybe for good). The client still allows you to connect and exchange messages with a Chromecast dongle without any restriction.

Installation

$ npm install castv2

On windows, to avoid native modules dependencies, use

$ npm install castv2 --no-optional

Usage

varClient=require('castv2').Client;varmdns=require('mdns');varbrowser=mdns.createBrowser(mdns.tcp('googlecast'));browser.on('serviceUp',function(service){console.log('found device %s at %s:%d',service.name,service.addresses[0],service.port);ondeviceup(service.addresses[0]);browser.stop();});browser.start();functionondeviceup(host){varclient=newClient();client.connect(host,function(){// create various namespace handlersvarconnection=client.createChannel('sender-0','receiver-0','urn:x-cast:com.google.cast.tp.connection','JSON');varheartbeat=client.createChannel('sender-0','receiver-0','urn:x-cast:com.google.cast.tp.heartbeat','JSON');varreceiver=client.createChannel('sender-0','receiver-0','urn:x-cast:com.google.cast.receiver','JSON');// establish virtual connection to the receiverconnection.send({type: 'CONNECT'});// start heartbeatingsetInterval(function(){heartbeat.send({type: 'PING'});},5000);// launch YouTube appreceiver.send({type: 'LAUNCH',appId: 'YouTube',requestId: 1});// display receiver status updatesreceiver.on('message',function(data,broadcast){if(data.type='RECEIVER_STATUS'){console.log(data.status);}});});}

Run it with the following command to get a full trace of the messages exchanged with the dongle.

$ DEBUG=* node example.js

Protocol description

This is an attempt at documenting the low-level protocol. I hope it will give sender-app makers a clearer picture of what is happening behind the curtain, and give the others ideas about how this kind of protocol can be implemented. The information presented here has been collated from various internet sources (mainly example code and other attempts to implement the protocol) and my own trial and error. Correct me as needed as I may have gotten concepts or namings wrong.

The TLS / Protocol Buffers layer

The client connects to the Chromecast through TLS on port 8009. Once the connection is established server and client exchange length-prefixed binary messages (that we'll call packets).

Packets have the following structure :

+----------------+------------------------------------------------+
| Packet length | Payload (message) |
+----------------+------------------------------------------------+

Packet length is a 32 bits Big Endian Unsigned Integer (UInt32BE in nodejs parlance) that determines the payload size.

Messages are serialized with Protocol Buffers and structured as follows (excerpt of cast_channel.proto with comments stripped) :

messageCastMessage {
enumProtocolVersion {
CASTV2_1_0=0;
}
requiredProtocolVersionprotocol_version=1;
requiredstringsource_id=2;
requiredstringdestination_id=3;
requiredstringnamespace=4;
enumPayloadType {
STRING=0;
BINARY=1;
}
requiredPayloadTypepayload_type=5;
optionalstringpayload_utf8=6;
optionalbytespayload_binary=7;
}

The original .proto file can also be found in the Chromium source tree.

Using this structure the sender and receiver platforms (eg. The Chrome browser and the Chromecast device) as well as sender and receiver applications (eg. a Chromecast receiver app and a Chrome browser sender app for YouTube) communicate on channels.

Senders and receivers identify themselves through IDs : source_id and destination_id. The sending platform (eg. the Chrome browser) usually uses sender-0. The receiving platform (the Chromecast dongle) uses receiver-0. Other senders and receivers use identifiers such as sender-sdqo7ozi6s4a, client-4637 or web-4. We'll dig into that later.

Namespaces

Senders and receivers communicate through channels defined by the namespace field. Each namespace corresponds to a protocol that can have its own semantics. Protocol-specific data is carried in the payload_utf8 or payload_binary fields. Either one or the other is present in the message depending on the payload_type field value. Thanks to that, applications can define their own protocols and transparently exchange arbitrary data, including binary data, alleviating the need to establish additional connections (ie. websockets).

Though, many protocols use JSON encoded messages / commands, which makes them easy to understand and implement.

Each sender or receiver can implement one or multiple protocols. For instance the Chromecast platform (receiver-0) implements the protocols for the following namespaces : urn:x-cast:com.google.cast.tp.connection, urn:x-cast:com.google.cast.tp.heartbeat, urn:x-cast:com.google.cast.receiver and urn:x-cast:com.google.cast.tp.deviceauth.

Communicating with receivers

Before being able to exchange messages with a receiver (be it an application or the platform), a sender must establish a virtual connection with it. This is accomplished through the urn:x-cast:com.google.cast.tp.connection namespace / protocol. This has the effect of both allowing the sender to send messages to the receiver, and of subscribing the sender to the receiver's broadcasts (eg. status updates).

The protocol is JSON encoded and the semantics are pretty simple :

Message payloadDescription
{ "type": "CONNECT" }establishes a virtual connection between the sender and the receiver
{ "type": "CLOSE" }closes a virtual connection

The sender may receive a CLOSE message from the receiver that terminates the virtual connection. This sometimes happens in error cases.

Once the virtual connection is established messages can be exchanged. Broadcasts from the receiver will have a * value for the destination_id field.

Keeping the connection alive

Connections are kept alive through the urn:x-cast:com.google.cast.tp.heartbeat namespace / protocol. At regular intervals, the sender must send a PING message that will get answered by a PONG. The protocol is JSON encoded.

Message payloadDescription
{ "type": "PING" }notifies the other end that we are sill alive
{ "type": "PONG" }the other end acknowledges that we are

Failing to do so will lead to connection termination. The default interval seems to be 5 seconds. This protocol allows the Chromecast to detect unresponsive / offline senders much quicker than the TCP keepalive mechanism.

Device authentication

Device authentication enables a sender to authenticate a Chromecast device. Authenticating the device is purely optional from a sender's perspective, though the official SDK libraries do it to prevent rogue Chromecast devices to communicate with the official sender platforms. Device authentication is taken care of by the urn:x-cast:com.google.cast.tp.deviceauth namespace / protocol.

First, the sender sends a challenge message to the platform receiver receiver-0 which responds by either a response message containing a signature, certificate and a variable number of certificate authority certificates that the sent certificate is verified against or an error message. These 3 payloads are protocol buffers encoded and described in cast_channel.proto as follows :

messageAuthChallenge {
}
messageAuthResponse {
requiredbytessignature=1;
requiredbytesclient_auth_certificate=2;
repeatedbytesclient_ca=3;
}
messageAuthError {
enumErrorType {
INTERNAL_ERROR=0;
NO_TLS=1; // The underlying connection is not TLS
}
requiredErrorTypeerror_type=1;
}
messageDeviceAuthMessage {
optionalAuthChallengechallenge=1;
optionalAuthResponseresponse=2;
optionalAuthErrorerror=3;
}

The challenge message is empty in the current version of the protocol (CAST v2.1.0), yet official sender platforms are checking the returned certificate and signature. Details of the verification process can be found in this issue.

Controlling applications

The platform receiver receiver-0 implements the urn:x-cast:com.google.cast.receiver namespace / protocol which provides an interface to launch, stop, and query the status of running applications. receiver-0 also broadcast status messages on this namespace when other senders launch, stop, or affect the status of running apps. It also allows checking the app for availability.

The protocol is JSON encoded and is request / response based. Requests include a type field containing the type of the request, namely LAUNCH, STOP, GET_STATUS and GET_APP_AVAILABILITY, and a requestId field that will be reflected in the receiver's response and allows the sender to pair request and responses. requestId is not shown in the table below but must be present in every request. In the wild, it is an initially random integer that gets incremented for each subsequent request.

Message payloadDescription
{ "type": "LAUNCH", appId: <string> }launches an application
{ "type": "STOP", sessionId: <string> }stops a running instance of an application
{ "type": "GET_STATUS" }returns the status of the platform receiver, including details about running apps.
{ "type": "GET_APP_AVAILABILITY", appId: <array> }returns availability of requested apps. appId is an array of application IDs.

appId may be eg. YouTube or CC1AD845 for the Default Media Receiver app. A sessionId identifies a running instance of an application and is provided in status messages.

As these requests affect the receiver's status they all return a RECEIVER_STATUS message of the following form :

{
"requestId": 8476438,
"status": { "applications": [
{ "appId": "CC1AD845",
"displayName": "Default Media Receiver",
"namespaces": [ "urn:x-cast:com.google.cast.player.message",
"urn:x-cast:com.google.cast.media"
],
"sessionId": "7E2FF513-CDF6-9A91-2B28-3E3DE7BAC174",
"statusText": "Ready To Cast",
"transportId": "web-5" }
],
"isActiveInput": true,
"volume": { "level": 1,
"muted": false }
},
"type": "RECEIVER_STATUS"
}

This response indicates an instance of the Default Media Receiver is running with sessionId 7E2FF513-CDF6-9A91-2B28-3E3DE7BAC174. namespaces indicates which protocols are supported by the running app. This could allow any sender application implementing the media protocol to control playback on this session.

Another important field here is transportId as it is the destinationId to be used to communicate with the app. Note that the app being a receiver like any other you must issue it a CONNECT message through the urn:x-cast:com.google.cast.tp.connection protocol before being able to send messages. In this case, this will have the side effect of subscribing you to media updates (on the media channel) of this Default Media Player session.

You can join an existing session (launched by another sender) by issuing the same CONNECT message.

Controlling device volume

receiver-0 allows setting volume and muting at the device-level through the SET_VOLUME request on urn:x-cast:com.google.cast.receiver.

Message payloadDescription
{ "type": "SET_VOLUME", "volume": { level: <float> } }sets volume. level is a float between 0 and 1
{ "type": "SET_VOLUME", "volume": { muted: <boolean> } }mutes / unmutes. muted is true or false

Contributors

About

An implementation of the Chromecast CASTV2 protocol

Resources

Stars

794 stars

Watchers

53 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

92 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

castv2

NPM versionDependency Statusnpm

An implementation of the Chromecast CASTV2 protocol

This module is an implementation of the Chromecast CASTV2 protocol over TLS. The internet is very sparse on information about the new Chromecast protocol so big props go to github.com/vincentbernat and his nodecastor module that helped me start off on the right foot and save a good deal of time in my research.

The module provides both a Client and a Server implementation of the low-level protocol. The server is (sadly) pretty useless because device authentication gets in the way for now (and maybe for good). The client still allows you to connect and exchange messages with a Chromecast dongle without any restriction.

Installation

$ npm install castv2

On windows, to avoid native modules dependencies, use

$ npm install castv2 --no-optional

Usage

varClient=require('castv2').Client;varmdns=require('mdns');varbrowser=mdns.createBrowser(mdns.tcp('googlecast'));browser.on('serviceUp',function(service){console.log('found device %s at %s:%d',service.name,service.addresses[0],service.port);ondeviceup(service.addresses[0]);browser.stop();});browser.start();functionondeviceup(host){varclient=newClient();client.connect(host,function(){// create various namespace handlersvarconnection=client.createChannel('sender-0','receiver-0','urn:x-cast:com.google.cast.tp.connection','JSON');varheartbeat=client.createChannel('sender-0','receiver-0','urn:x-cast:com.google.cast.tp.heartbeat','JSON');varreceiver=client.createChannel('sender-0','receiver-0','urn:x-cast:com.google.cast.receiver','JSON');// establish virtual connection to the receiverconnection.send({type: 'CONNECT'});// start heartbeatingsetInterval(function(){heartbeat.send({type: 'PING'});},5000);// launch YouTube appreceiver.send({type: 'LAUNCH',appId: 'YouTube',requestId: 1});// display receiver status updatesreceiver.on('message',function(data,broadcast){if(data.type='RECEIVER_STATUS'){console.log(data.status);}});});}

Run it with the following command to get a full trace of the messages exchanged with the dongle.

$ DEBUG=* node example.js

Protocol description

This is an attempt at documenting the low-level protocol. I hope it will give sender-app makers a clearer picture of what is happening behind the curtain, and give the others ideas about how this kind of protocol can be implemented. The information presented here has been collated from various internet sources (mainly example code and other attempts to implement the protocol) and my own trial and error. Correct me as needed as I may have gotten concepts or namings wrong.

The TLS / Protocol Buffers layer

The client connects to the Chromecast through TLS on port 8009. Once the connection is established server and client exchange length-prefixed binary messages (that we'll call packets).

Packets have the following structure :

+----------------+------------------------------------------------+
| Packet length | Payload (message) |
+----------------+------------------------------------------------+

Packet length is a 32 bits Big Endian Unsigned Integer (UInt32BE in nodejs parlance) that determines the payload size.

Messages are serialized with Protocol Buffers and structured as follows (excerpt of cast_channel.proto with comments stripped) :

messageCastMessage {
enumProtocolVersion {
CASTV2_1_0=0;
}
requiredProtocolVersionprotocol_version=1;
requiredstringsource_id=2;
requiredstringdestination_id=3;
requiredstringnamespace=4;
enumPayloadType {
STRING=0;
BINARY=1;
}
requiredPayloadTypepayload_type=5;
optionalstringpayload_utf8=6;
optionalbytespayload_binary=7;
}

The original .proto file can also be found in the Chromium source tree.

Using this structure the sender and receiver platforms (eg. The Chrome browser and the Chromecast device) as well as sender and receiver applications (eg. a Chromecast receiver app and a Chrome browser sender app for YouTube) communicate on channels.

Senders and receivers identify themselves through IDs : source_id and destination_id. The sending platform (eg. the Chrome browser) usually uses sender-0. The receiving platform (the Chromecast dongle) uses receiver-0. Other senders and receivers use identifiers such as sender-sdqo7ozi6s4a, client-4637 or web-4. We'll dig into that later.

Namespaces

Senders and receivers communicate through channels defined by the namespace field. Each namespace corresponds to a protocol that can have its own semantics. Protocol-specific data is carried in the payload_utf8 or payload_binary fields. Either one or the other is present in the message depending on the payload_type field value. Thanks to that, applications can define their own protocols and transparently exchange arbitrary data, including binary data, alleviating the need to establish additional connections (ie. websockets).

Though, many protocols use JSON encoded messages / commands, which makes them easy to understand and implement.

Each sender or receiver can implement one or multiple protocols. For instance the Chromecast platform (receiver-0) implements the protocols for the following namespaces : urn:x-cast:com.google.cast.tp.connection, urn:x-cast:com.google.cast.tp.heartbeat, urn:x-cast:com.google.cast.receiver and urn:x-cast:com.google.cast.tp.deviceauth.

Communicating with receivers

Before being able to exchange messages with a receiver (be it an application or the platform), a sender must establish a virtual connection with it. This is accomplished through the urn:x-cast:com.google.cast.tp.connection namespace / protocol. This has the effect of both allowing the sender to send messages to the receiver, and of subscribing the sender to the receiver's broadcasts (eg. status updates).

The protocol is JSON encoded and the semantics are pretty simple :

Message payloadDescription
{ "type": "CONNECT" }establishes a virtual connection between the sender and the receiver
{ "type": "CLOSE" }closes a virtual connection

The sender may receive a CLOSE message from the receiver that terminates the virtual connection. This sometimes happens in error cases.

Once the virtual connection is established messages can be exchanged. Broadcasts from the receiver will have a * value for the destination_id field.

Keeping the connection alive

Connections are kept alive through the urn:x-cast:com.google.cast.tp.heartbeat namespace / protocol. At regular intervals, the sender must send a PING message that will get answered by a PONG. The protocol is JSON encoded.

Message payloadDescription
{ "type": "PING" }notifies the other end that we are sill alive
{ "type": "PONG" }the other end acknowledges that we are

Failing to do so will lead to connection termination. The default interval seems to be 5 seconds. This protocol allows the Chromecast to detect unresponsive / offline senders much quicker than the TCP keepalive mechanism.

Device authentication

Device authentication enables a sender to authenticate a Chromecast device. Authenticating the device is purely optional from a sender's perspective, though the official SDK libraries do it to prevent rogue Chromecast devices to communicate with the official sender platforms. Device authentication is taken care of by the urn:x-cast:com.google.cast.tp.deviceauth namespace / protocol.

First, the sender sends a challenge message to the platform receiver receiver-0 which responds by either a response message containing a signature, certificate and a variable number of certificate authority certificates that the sent certificate is verified against or an error message. These 3 payloads are protocol buffers encoded and described in cast_channel.proto as follows :

messageAuthChallenge {
}
messageAuthResponse {
requiredbytessignature=1;
requiredbytesclient_auth_certificate=2;
repeatedbytesclient_ca=3;
}
messageAuthError {
enumErrorType {
INTERNAL_ERROR=0;
NO_TLS=1; // The underlying connection is not TLS
}
requiredErrorTypeerror_type=1;
}
messageDeviceAuthMessage {
optionalAuthChallengechallenge=1;
optionalAuthResponseresponse=2;
optionalAuthErrorerror=3;
}

The challenge message is empty in the current version of the protocol (CAST v2.1.0), yet official sender platforms are checking the returned certificate and signature. Details of the verification process can be found in this issue.

Controlling applications

The platform receiver receiver-0 implements the urn:x-cast:com.google.cast.receiver namespace / protocol which provides an interface to launch, stop, and query the status of running applications. receiver-0 also broadcast status messages on this namespace when other senders launch, stop, or affect the status of running apps. It also allows checking the app for availability.

The protocol is JSON encoded and is request / response based. Requests include a type field containing the type of the request, namely LAUNCH, STOP, GET_STATUS and GET_APP_AVAILABILITY, and a requestId field that will be reflected in the receiver's response and allows the sender to pair request and responses. requestId is not shown in the table below but must be present in every request. In the wild, it is an initially random integer that gets incremented for each subsequent request.

Message payloadDescription
{ "type": "LAUNCH", appId: <string> }launches an application
{ "type": "STOP", sessionId: <string> }stops a running instance of an application
{ "type": "GET_STATUS" }returns the status of the platform receiver, including details about running apps.
{ "type": "GET_APP_AVAILABILITY", appId: <array> }returns availability of requested apps. appId is an array of application IDs.

appId may be eg. YouTube or CC1AD845 for the Default Media Receiver app. A sessionId identifies a running instance of an application and is provided in status messages.

As these requests affect the receiver's status they all return a RECEIVER_STATUS message of the following form :

{
"requestId": 8476438,
"status": { "applications": [
{ "appId": "CC1AD845",
"displayName": "Default Media Receiver",
"namespaces": [ "urn:x-cast:com.google.cast.player.message",
"urn:x-cast:com.google.cast.media"
],
"sessionId": "7E2FF513-CDF6-9A91-2B28-3E3DE7BAC174",
"statusText": "Ready To Cast",
"transportId": "web-5" }
],
"isActiveInput": true,
"volume": { "level": 1,
"muted": false }
},
"type": "RECEIVER_STATUS"
}

This response indicates an instance of the Default Media Receiver is running with sessionId 7E2FF513-CDF6-9A91-2B28-3E3DE7BAC174. namespaces indicates which protocols are supported by the running app. This could allow any sender application implementing the media protocol to control playback on this session.

Another important field here is transportId as it is the destinationId to be used to communicate with the app. Note that the app being a receiver like any other you must issue it a CONNECT message through the urn:x-cast:com.google.cast.tp.connection protocol before being able to send messages. In this case, this will have the side effect of subscribing you to media updates (on the media channel) of this Default Media Player session.

You can join an existing session (launched by another sender) by issuing the same CONNECT message.

Controlling device volume

receiver-0 allows setting volume and muting at the device-level through the SET_VOLUME request on urn:x-cast:com.google.cast.receiver.

Message payloadDescription
{ "type": "SET_VOLUME", "volume": { level: <float> } }sets volume. level is a float between 0 and 1
{ "type": "SET_VOLUME", "volume": { muted: <boolean> } }mutes / unmutes. muted is true or false

Contributors

About

An implementation of the Chromecast CASTV2 protocol

Resources

Stars

794 stars

Watchers

53 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Latest commit

History

92 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

castv2

NPM versionDependency Statusnpm

An implementation of the Chromecast CASTV2 protocol

This module is an implementation of the Chromecast CASTV2 protocol over TLS. The internet is very sparse on information about the new Chromecast protocol so big props go to github.com/vincentbernat and his nodecastor module that helped me start off on the right foot and save a good deal of time in my research.

The module provides both a Client and a Server implementation of the low-level protocol. The server is (sadly) pretty useless because device authentication gets in the way for now (and maybe for good). The client still allows you to connect and exchange messages with a Chromecast dongle without any restriction.

Installation

$ npm install castv2

On windows, to avoid native modules dependencies, use

$ npm install castv2 --no-optional

Usage

varClient=require('castv2').Client;varmdns=require('mdns');varbrowser=mdns.createBrowser(mdns.tcp('googlecast'));browser.on('serviceUp',function(service){console.log('found device %s at %s:%d',service.name,service.addresses[0],service.port);ondeviceup(service.addresses[0]);browser.stop();});browser.start();functionondeviceup(host){varclient=newClient();client.connect(host,function(){// create various namespace handlersvarconnection=client.createChannel('sender-0','receiver-0','urn:x-cast:com.google.cast.tp.connection','JSON');varheartbeat=client.createChannel('sender-0','receiver-0','urn:x-cast:com.google.cast.tp.heartbeat','JSON');varreceiver=client.createChannel('sender-0','receiver-0','urn:x-cast:com.google.cast.receiver','JSON');// establish virtual connection to the receiverconnection.send({type: 'CONNECT'});// start heartbeatingsetInterval(function(){heartbeat.send({type: 'PING'});},5000);// launch YouTube appreceiver.send({type: 'LAUNCH',appId: 'YouTube',requestId: 1});// display receiver status updatesreceiver.on('message',function(data,broadcast){if(data.type='RECEIVER_STATUS'){console.log(data.status);}});});}

Run it with the following command to get a full trace of the messages exchanged with the dongle.

$ DEBUG=* node example.js

Protocol description

This is an attempt at documenting the low-level protocol. I hope it will give sender-app makers a clearer picture of what is happening behind the curtain, and give the others ideas about how this kind of protocol can be implemented. The information presented here has been collated from various internet sources (mainly example code and other attempts to implement the protocol) and my own trial and error. Correct me as needed as I may have gotten concepts or namings wrong.

The TLS / Protocol Buffers layer

The client connects to the Chromecast through TLS on port 8009. Once the connection is established server and client exchange length-prefixed binary messages (that we'll call packets).

Packets have the following structure :

+----------------+------------------------------------------------+
| Packet length | Payload (message) |
+----------------+------------------------------------------------+

Packet length is a 32 bits Big Endian Unsigned Integer (UInt32BE in nodejs parlance) that determines the payload size.

Messages are serialized with Protocol Buffers and structured as follows (excerpt of cast_channel.proto with comments stripped) :

messageCastMessage {
enumProtocolVersion {
CASTV2_1_0=0;
}
requiredProtocolVersionprotocol_version=1;
requiredstringsource_id=2;
requiredstringdestination_id=3;
requiredstringnamespace=4;
enumPayloadType {
STRING=0;
BINARY=1;
}
requiredPayloadTypepayload_type=5;
optionalstringpayload_utf8=6;
optionalbytespayload_binary=7;
}

The original .proto file can also be found in the Chromium source tree.

Using this structure the sender and receiver platforms (eg. The Chrome browser and the Chromecast device) as well as sender and receiver applications (eg. a Chromecast receiver app and a Chrome browser sender app for YouTube) communicate on channels.

Senders and receivers identify themselves through IDs : source_id and destination_id. The sending platform (eg. the Chrome browser) usually uses sender-0. The receiving platform (the Chromecast dongle) uses receiver-0. Other senders and receivers use identifiers such as sender-sdqo7ozi6s4a, client-4637 or web-4. We'll dig into that later.

Namespaces

Senders and receivers communicate through channels defined by the namespace field. Each namespace corresponds to a protocol that can have its own semantics. Protocol-specific data is carried in the payload_utf8 or payload_binary fields. Either one or the other is present in the message depending on the payload_type field value. Thanks to that, applications can define their own protocols and transparently exchange arbitrary data, including binary data, alleviating the need to establish additional connections (ie. websockets).

Though, many protocols use JSON encoded messages / commands, which makes them easy to understand and implement.

Each sender or receiver can implement one or multiple protocols. For instance the Chromecast platform (receiver-0) implements the protocols for the following namespaces : urn:x-cast:com.google.cast.tp.connection, urn:x-cast:com.google.cast.tp.heartbeat, urn:x-cast:com.google.cast.receiver and urn:x-cast:com.google.cast.tp.deviceauth.

Communicating with receivers

Before being able to exchange messages with a receiver (be it an application or the platform), a sender must establish a virtual connection with it. This is accomplished through the urn:x-cast:com.google.cast.tp.connection namespace / protocol. This has the effect of both allowing the sender to send messages to the receiver, and of subscribing the sender to the receiver's broadcasts (eg. status updates).

The protocol is JSON encoded and the semantics are pretty simple :

Message payloadDescription
{ "type": "CONNECT" }establishes a virtual connection between the sender and the receiver
{ "type": "CLOSE" }closes a virtual connection

The sender may receive a CLOSE message from the receiver that terminates the virtual connection. This sometimes happens in error cases.

Once the virtual connection is established messages can be exchanged. Broadcasts from the receiver will have a * value for the destination_id field.

Keeping the connection alive

Connections are kept alive through the urn:x-cast:com.google.cast.tp.heartbeat namespace / protocol. At regular intervals, the sender must send a PING message that will get answered by a PONG. The protocol is JSON encoded.

Message payloadDescription
{ "type": "PING" }notifies the other end that we are sill alive
{ "type": "PONG" }the other end acknowledges that we are

Failing to do so will lead to connection termination. The default interval seems to be 5 seconds. This protocol allows the Chromecast to detect unresponsive / offline senders much quicker than the TCP keepalive mechanism.

Device authentication

Device authentication enables a sender to authenticate a Chromecast device. Authenticating the device is purely optional from a sender's perspective, though the official SDK libraries do it to prevent rogue Chromecast devices to communicate with the official sender platforms. Device authentication is taken care of by the urn:x-cast:com.google.cast.tp.deviceauth namespace / protocol.

First, the sender sends a challenge message to the platform receiver receiver-0 which responds by either a response message containing a signature, certificate and a variable number of certificate authority certificates that the sent certificate is verified against or an error message. These 3 payloads are protocol buffers encoded and described in cast_channel.proto as follows :

messageAuthChallenge {
}
messageAuthResponse {
requiredbytessignature=1;
requiredbytesclient_auth_certificate=2;
repeatedbytesclient_ca=3;
}
messageAuthError {
enumErrorType {
INTERNAL_ERROR=0;
NO_TLS=1; // The underlying connection is not TLS
}
requiredErrorTypeerror_type=1;
}
messageDeviceAuthMessage {
optionalAuthChallengechallenge=1;
optionalAuthResponseresponse=2;
optionalAuthErrorerror=3;
}

The challenge message is empty in the current version of the protocol (CAST v2.1.0), yet official sender platforms are checking the returned certificate and signature. Details of the verification process can be found in this issue.

Controlling applications

The platform receiver receiver-0 implements the urn:x-cast:com.google.cast.receiver namespace / protocol which provides an interface to launch, stop, and query the status of running applications. receiver-0 also broadcast status messages on this namespace when other senders launch, stop, or affect the status of running apps. It also allows checking the app for availability.

The protocol is JSON encoded and is request / response based. Requests include a type field containing the type of the request, namely LAUNCH, STOP, GET_STATUS and GET_APP_AVAILABILITY, and a requestId field that will be reflected in the receiver's response and allows the sender to pair request and responses. requestId is not shown in the table below but must be present in every request. In the wild, it is an initially random integer that gets incremented for each subsequent request.

Message payloadDescription
{ "type": "LAUNCH", appId: <string> }launches an application
{ "type": "STOP", sessionId: <string> }stops a running instance of an application
{ "type": "GET_STATUS" }returns the status of the platform receiver, including details about running apps.
{ "type": "GET_APP_AVAILABILITY", appId: <array> }returns availability of requested apps. appId is an array of application IDs.

appId may be eg. YouTube or CC1AD845 for the Default Media Receiver app. A sessionId identifies a running instance of an application and is provided in status messages.

As these requests affect the receiver's status they all return a RECEIVER_STATUS message of the following form :

{
"requestId": 8476438,
"status": { "applications": [
{ "appId": "CC1AD845",
"displayName": "Default Media Receiver",
"namespaces": [ "urn:x-cast:com.google.cast.player.message",
"urn:x-cast:com.google.cast.media"
],
"sessionId": "7E2FF513-CDF6-9A91-2B28-3E3DE7BAC174",
"statusText": "Ready To Cast",
"transportId": "web-5" }
],
"isActiveInput": true,
"volume": { "level": 1,
"muted": false }
},
"type": "RECEIVER_STATUS"
}

This response indicates an instance of the Default Media Receiver is running with sessionId 7E2FF513-CDF6-9A91-2B28-3E3DE7BAC174. namespaces indicates which protocols are supported by the running app. This could allow any sender application implementing the media protocol to control playback on this session.

Another important field here is transportId as it is the destinationId to be used to communicate with the app. Note that the app being a receiver like any other you must issue it a CONNECT message through the urn:x-cast:com.google.cast.tp.connection protocol before being able to send messages. In this case, this will have the side effect of subscribing you to media updates (on the media channel) of this Default Media Player session.

You can join an existing session (launched by another sender) by issuing the same CONNECT message.

Controlling device volume

receiver-0 allows setting volume and muting at the device-level through the SET_VOLUME request on urn:x-cast:com.google.cast.receiver.

Message payloadDescription
{ "type": "SET_VOLUME", "volume": { level: <float> } }sets volume. level is a float between 0 and 1
{ "type": "SET_VOLUME", "volume": { muted: <boolean> } }mutes / unmutes. muted is true or false

Contributors

About

An implementation of the Chromecast CASTV2 protocol

Resources

Stars

794 stars

Watchers

53 watching

Forks

Releases

Packages

Used by

Contributors

Languages