Google TV (aka Android TV) Remote Control (v2) - Aymkdn/assistant-freebox-cloud GitHub Wiki
This page explains the protocol ATV Remote v2 (used since September 2021 by Google with its Remote Service v5): how we can pair with a remote Android/Google TV and how to send commands (like changing the channel, the volume, etc). Thanks to @hubertlejaune for his tremendous help.
Requirements
The Android TV (aka server
in this document) should have 2 open ports: 6466 and 6467.
To know more about the Android TV, we can enter the below Linux command:
openssl s_client -connect SERVER_IP:6467 -prexit -state -debug
Which will return some information, including the server's public certificate that we'll need later.
If you only want the server's public certificate:
openssl s_client -showcerts -connect SERVER_IP:6467 </dev/null 2>/dev/null|openssl x509 -outform PEM > server.pem
Note
- if you get a negative number, then you must convert the unsigned decimal to a signed one, using https://onlinetoolz.net/unsigned-signed#base=10&value=-14&bits=8. For example,
-56
is actually200
. - you can use https://www.rapidtables.com/convert/number/ascii-hex-bin-dec-converter.html to convert between decimal and ascii.
- you can find a JavaScript implementation here: https://github.com/louis49/androidtv-remote
Pairing
The pairing protocol will happen on port 6467.
Client's certificate
It's required to generate our own (client) certificate.
In PHP we can do it with the below code:
<?php
// the commande line is: php generate_key.php > client.pem
// certificate details (Distinguished Name)
// (OpenSSL applies defaults to missing fields)
$dn = array(
"commonName" => "atvremote",
"countryName" => "US",
"stateOrProvinceName" => "California",
"localityName" => "Montain View",
"organizationName" => "Google Inc.",
"organizationalUnitName" => "Android",
"emailAddress" => "[email protected]"
);
// create certificate which is valid for ~10 years
$privkey = openssl_pkey_new();
$cert = openssl_csr_new($dn, $privkey);
$cert = openssl_csr_sign($cert, null, $privkey, 3650);
// export public key
openssl_x509_export($cert, $out);
echo $out;
// export private key
$passphrase = null;
openssl_pkey_export($privkey, $out, $passphrase);
echo $out;
It will generate a file called client.pem
that contains both the public and the private keys for our client.
Connection to the server
You need to open a TLS/SSL connection to the server using port 6467.
In PHP, you could use https://github.com/reactphp/socket:
<?php
use React\EventLoop\Factory;
use React\Socket\Connector;
use React\Socket\SecureConnector;
use React\Socket\ConnectionInterface;
require __DIR__ . '/./vendor/autoload.php';
$host = 'SERVER_IP';
$loop = Factory::create();
$tcpConnector = new React\Socket\TcpConnector($loop);
$dnsResolverFactory = new React\Dns\Resolver\Factory();
$dns = $dnsResolverFactory->createCached('8.8.8.8', $loop);
$dnsConnector = new React\Socket\DnsConnector($tcpConnector, $dns);
$connector = new SecureConnector($dnsConnector, $loop, array(
'allow_self_signed' => true,
'verify_peer' => false,
'verify_peer_name' => false,
'dns' => false,
'local_cert' => 'client.pem'
));
$connector->connect('tls://' . $host . ':6467')->then(function (ConnectionInterface $connection) use ($host) {
$connection->on('data', function ($data) use ($connection) {
$dataLen = strlen($data);
echo "data recv => ".$data." (".strlen($data).")\n";
// deal with the messages received from the server
});
// below we can send the first message
$connection->write(/* first message here */);
}, 'printf');
$loop->run();
?>
1) Send the Pairing message
We need to build the payload with an array of bytes:
8, 2
: it's the protocol version 216, 200, 1
: it's the status codeOK
82
: it's the message tag43
: it's the length of the message10
: it's the service name tagLENGTH_OF_NEXT_STRING, BYTES_OF_THE_STRING_THAT_IS_THE_SERVICE_NAME
: it's the service name
e.g.[21, 105,110,102,111,46,107,111,100,111,110,111,46,97,115,115,105,115,116,97,110,116]
, with21
the size, and105,110,102,111,46,107,111,100,111,110,111,46,97,115,115,105,115,116,97,110,116
forinfo.kodono.assistant
18
: it's the tag device nameLENGTH_OF_NEXT_STRING, BYTES_OF_THE_STRING_THAT_IS_THE_CLIENT_NAME
: it's the client name
e.g.[13, 105, 110, 116, 101, 114, 102, 97, 99, 101, 32, 119, 101, 98]
, with13
the size, and105, 110, 116, 101, 114, 102, 97, 99, 101, 32, 119, 101, 98
forinterface web
So we need to send a first message with the size of our payload, and then the payload:
[ 45 ]
[ 8, 2, 16, 200, 1, 82, 43, 10, 21, 105, 110, 102, 111, 46, 107, 111, 100, 111, 110, 111, 46, 97, 115, 115, 105, 115, 116, 97, 110, 116, 18, 13, 105, 110, 116, 101, 114, 102, 97, 99, 101, 32, 119, 101, 98 ]
The server acknowledges with the 2 messages, one that is the size, and then the message itself:
[ 7 ]
[ 8, 2, 16, 200, 1, 90, 0 ]
We can split the response:
8, 2
: it's the protocol version 216, 200, 1
: it's the status codeOK
(it could be16, 144, 3
forERROR
, or16, 145, 3
forBAD_CONFIGURATION
)
2) Send the Option message
We respond with the below payload:
8, 2
: it's the protocol version 216, 200, 1
: it's the status codeOK
162
: it's the message tag??1
: ??8
: it's the encoding output??10
: ??4
: the size??8
: it's the tag type??3
: it's the encoding type (0
forENCODING_TYPE_UNKNOWN
,1
forENCODING_TYPE_ALPHANUMERIC
,2
forENCODING_TYPE_NUMERIC
,3
forENCODING_TYPE_HEXADECIMAL
,4
forENCODING_TYPE_QRCODE
)16
: it's the size tag??6
: it's the symbol length??24
: it's the preferred role tag??1
: it's the preferred role (1
forROLE_TYPE_INPUT
)
So we need to send a first message with the size of our payload, and then the payload:
[ 16 ]
[ 8, 2, 16, 200, 1, 162, 1, 8, 10, 4, 8, 3, 16, 6, 24, 1 ]
The server acknowledges with the 2 messages, one that is the size, and then the message itself:
[ 16 ]
[ 8, 2, 16, 200, 1, 162, 1, 8, 18, 4, 8, 3, 16, 6, 24, 1 ]
3) Send the Configuration message
We respond with the below payload:
8, 2
: it's the protocol version 216, 200, 1
: it's the status codeOK
242
: it's the message tag??1
: ??8
: it's the encoding tag??10
: ??4
: it's the size??8
: it's the type tag3
: it's the protocol encoding (0
forENCODING_TYPE_UNKNOWN
,1
forENCODING_TYPE_ALPHANUMERIC
,2
forENCODING_TYPE_NUMERIC
,3
forENCODING_TYPE_HEXADECIMAL
,4
forENCODING_TYPE_QRCODE
)16
: it's the size tag??6
: it's the symbol length??16
: it's the preferred role tag??1
: it's the preferred role (1
forROLE_TYPE_INPUT
)
So we need to send a first message with the size of our payload, and then the payload:
[ 16 ]
[ 8, 2, 16, 200, 1, 242, 1, 8, 10, 4, 8, 3, 16, 6, 16, 1 ]
The server acknowledges with the 2 messages, one that is the size, and then the message itself:
[ 8 ]
[ 8, 2, 16, 200, 1, 250, 1, 0 ]
The TV screen should display a code with 6 characters.
4) Send the secret
Encode the secret
We first need to encode the secret.
To find the encoded secret:
- we use a SHA-256 hash;
- we add the client public key's
modulus
to the hash; - we add the client public key's
exponent
to the hash; - we add the server public key's
modulus
to the hash; - we add the server public key's
exponent
to the hash; - we add the last 4 characters of the code (the one displayed on the TV screen) to the hash.
In Java, the function looks like that:
public byte[] computeAlphaValue(byte[] bArr) {
PublicKey publicKey = this.clientCertificate.getPublicKey();
PublicKey publicKey2 = this.serverCertificate.getPublicKey();
verboseDebug("computeAlphaValue, nonce=" + bytesToHexString(bArr));
if (!(publicKey instanceof RSAPublicKey) || !(publicKey2 instanceof RSAPublicKey)) {
Log.e(TAG, "Expecting RSA public key");
return null;
}
RSAPublicKey rSAPublicKey = (RSAPublicKey) publicKey;
RSAPublicKey rSAPublicKey2 = (RSAPublicKey) publicKey2;
try {
MessageDigest instance = MessageDigest.getInstance("SHA-256");
byte[] byteArray = rSAPublicKey.getModulus().abs().toByteArray();
byte[] byteArray2 = rSAPublicKey.getPublicExponent().abs().toByteArray();
byte[] byteArray3 = rSAPublicKey2.getModulus().abs().toByteArray();
byte[] byteArray4 = rSAPublicKey2.getPublicExponent().abs().toByteArray();
byte[] removeLeadingNullBytes = removeLeadingNullBytes(byteArray);
byte[] removeLeadingNullBytes2 = removeLeadingNullBytes(byteArray2);
byte[] removeLeadingNullBytes3 = removeLeadingNullBytes(byteArray3);
byte[] removeLeadingNullBytes4 = removeLeadingNullBytes(byteArray4);
verboseDebug("Hash inputs, in order: ");
verboseDebug(" client modulus: " + bytesToHexString(removeLeadingNullBytes));
verboseDebug(" client exponent: " + bytesToHexString(removeLeadingNullBytes2));
verboseDebug(" server modulus: " + bytesToHexString(removeLeadingNullBytes3));
verboseDebug(" server exponent: " + bytesToHexString(removeLeadingNullBytes4));
verboseDebug(" nonce: " + bytesToHexString(bArr));
instance.update(removeLeadingNullBytes);
instance.update(removeLeadingNullBytes2);
instance.update(removeLeadingNullBytes3);
instance.update(removeLeadingNullBytes4);
instance.update(bArr);
return instance.digest();
} catch (NoSuchAlgorithmException unused) {
Log.e(TAG, "no sha-256 implementation");
return null;
}
}
In PHP:
// get the client's certificate
$clientPub = openssl_get_publickey(file_get_contents(__DIR__.'/client.pem'));
$clientPubDetails = openssl_pkey_get_details($clientPub);
// get the server's certificate
$serverPub = openssl_get_publickey($serverCertificate);
$serverPubDetails = openssl_pkey_get_details($serverPub);
// get the client's certificate modulus
$clientModulus = $clientPubDetails['rsa']['n'];
// get the client's certificate exponent
$clientExponent = $clientPubDetails['rsa']['e'];
// get the server's certificate modulus
$serverModulus = $serverPubDetails['rsa']['n'];
// get the server's certificate exponent
$serverExponent = $serverPubDetails['rsa']['e'];
// use SHA-256
$ctxHash = hash_init('sha256');
hash_update($ctxHash, $clientModulus);
hash_update($ctxHash, $clientExponent);
hash_update($ctxHash, $serverModulus);
hash_update($ctxHash, $serverExponent);
// only keep the last four characters of the code
$codeBin = hex2bin(substr($code, 2, 4));
hash_update($ctxHash, $codeBin);
$alpha = hash_final($ctxHash, true);
// change it to an array of bytes that will use in the payload
$alphaHex = bin2hex($alpha);
for ($i=0; $i<strlen($alphaHex); $i+=2) {
array_push($payload, hexdec(substr($alphaHex, $i, 2)));
}
Send the encoded secret
We send it with the below payload:
8, 2
: it's the protocol version 216, 200, 1
: it's the status codeOK
194, 2, 34, 10
: ??32
: it's the size of the encoded secret*THE_ENCODED_SECRET_ON_32_BYTES
Don't forget to send the size of the message first (42
), before sending the payload.
The server replies with the same kind of encoding, because we're supposed to verify we're dealing with the correct server:
42
: the size of the message8, 2
: it's the protocol version 216, 200, 1
: it's the status codeOK
- several bytes…
Send commands
Now that the client is paired with the server, we'll use port 6466 to send the commands.
Three steps are required to send a command:
- Send the 1st configuration message
- Send the 2nd configuration message
- Send the command
1st Configuration Message
As soon as you're connected to the server, it will send you a message that contains some information about the server.
The received message should look like the below:
10
: tagSIZE_OF_THE_WHOLE_MESSAGE
8, 255, 4, 18
or8, 239, 4, 18
: ??SIZE_OF_THE_SUB_MESSAGE
10
: tagSIZE_OF_MODEL_NAME
MODEL_NAME_OF_TV
18
: tagSIZE_OF_VENDOR_NAME
VENDOR_NAME
24, 1, 34
: ??SIZE_OF_SOMETHING_VERSION
VERSION_NUMBER
42
: tagSIZE_OF_PACKAGE_NAME
APP_NAME
SIZE_OF_APP_VERSION
APP_VERSION
You now have to send a configuration message:
10
: tagSIZE_OF_THE_WHOLE_MESSAGE
8, 238, 4, 18
SIZE_OF_THE_SUB_MESSAGE
24, 1, 34
SIZE_OF_YOUR_APP_VERSION
YOUR_APP_VERSION_NUMBER
: e.g.1
becomes49
42
: tagSIZE_OF_PACKAGE_NAME
97, 110, 100, 114, 111, 105, 116, 118, 45, 114, 101, 109, 111, 116, 101
: that isandroidtv-remote
50
: tagSIZE_OF_APP_VERSION
APP_VERSION
: e.g.1.0.0
becomes49, 46, 48, 46, 48
We first send the size, and then the payload. Example of a valid message:
[ 36 ]
[ 10, 34, 8, 238, 4, 18, 29, 24, 1, 34, 1, 49, 42, 15, 97, 110, 100, 114, 111, 105, 116, 118, 45, 114, 101, 109, 111, 116, 101, 50, 5, 49, 46, 48, 46, 48 ]
The server returns:
[ 5 ]
[ 10, 3, 8, 255, 4 ]
[ 2 ]
[ 18,0 ]
2nd Configuration Message
After the response [18, 0]
from the server, we send a second payload:
18, 3, 8, 238, 4
We send the size first, and the payload:
[ 5 ]
[ 18, 3, 8, 238, 4 ]
The server will respond with 3 messages (that could arrive in a different order) providing server's info:
[ 5 ]
[ 194, 2, 2, 8, 1 ] // '1' indicates it's powered, and '0' would indicate it's off
[ 25 ]
[ 162, 1, 22, 10, 20, 98, 18, 111, 114, 103, 46, 100, 114, 111, 105, 100, 116, 118, 46, 112, 108, 97, 121, 116, 118 ] // indicates the current app
[ 18 ]
[ 146, 3, 15, 8, 9, 16, 10, 26, 7, 84, 80, 77, 49, 55, 49, 69, 32, 1 ] // indicates the player name and the volume level
Send the command
2 messages (and their size) must be sent for each command:
82,4,8
: the command tagKEY_EVENT
: the constant value from https://developer.android.com/reference/android/view/KeyEvent (e.g.24
forKEYCODE_VOLUME_UP
)16, 1
forpress
or16, 2
for release
For example, to increase the volume:
[ 6 ]
[ 82, 4, 8, 24, 16, 1 ]
[ 6 ]
[ 82, 4, 8, 24, 16, 2 ]
Some commands, like KEYCODE_CHANNEL_UP
and KEYCODE_CHANNEL_DOWN
will need only one message:
[ 7 ]
[ 82, 5, 8, $code, 1, 16, 3 ] // $code is e.g. 166 forKEYCODE_CHANNEL_UP
Ping/Pong
Please, note that the server will send 3 pings and if no pong is received, the connection will be closed.
A ping packet will start with [66,6
, and you may have to respond with [74, 2, 8, 25]
(no need to send the size).
Start an application
To launch an application:
210,5
: the command tagSIZE_WHOLE_MESSAGE
10
: tagSIZE_CONTENT
CONTENT
With CONTENT
that is a deeplink (the deeplink should appear in the AndroidManifest.xml with the value android:host
).
Example to launch Netflix, we use the deeplink https://www.netflix.com/title.*
:
- [ 36 ]
- [ 210, 5, 33, 10, 31, 104, 116, 116, 112, 115, 58, 47, 47, 119, 119, 119, 46, 110, 101, 116, 102, 108, 105, 120, 46, 99, 111, 109, 47, 116, 105, 116, 108, 101, 46, 42 ]
Text input from keyboard
I didn't implement the text input, so no need to ask me about it.
However, there is a JavaScript and a Python implementation of it that could help you.