-
-
Notifications
You must be signed in to change notification settings - Fork 14
Google TV (aka Android TV) Remote Control (v2)
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.
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
- 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
The pairing protocol will happen on port 6467.
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.
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();
?>
We need to build the payload with an array of bytes:
-
8, 2
: it's the protocol version 2 -
16, 200, 1
: it's the status codeOK
-
82
: it's the message tag -
43
: it's the length of the message -
10
: it's the service name tag -
LENGTH_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 name -
LENGTH_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 2 -
16, 200, 1
: it's the status codeOK
(it could be16, 144, 3
forERROR
, or16, 145, 3
forBAD_CONFIGURATION
)
We respond with the below payload:
-
8, 2
: it's the protocol version 2 -
16, 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 ]
We respond with the below payload:
-
8, 2
: it's the protocol version 2 -
16, 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 tag -
3
: 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.
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)));
}
We send it with the below payload:
-
8, 2
: it's the protocol version 2 -
16, 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 message -
8, 2
: it's the protocol version 2 -
16, 200, 1
: it's the status codeOK
- several bytes…
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
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
: tag SIZE_OF_THE_WHOLE_MESSAGE
-
8, 255, 4, 18
or8, 239, 4, 18
: ?? SIZE_OF_THE_SUB_MESSAGE
-
10
: tag SIZE_OF_MODEL_NAME
MODEL_NAME_OF_TV
-
18
: tag SIZE_OF_VENDOR_NAME
VENDOR_NAME
-
24, 1, 34
: ?? SIZE_OF_SOMETHING_VERSION
VERSION_NUMBER
-
42
: tag SIZE_OF_PACKAGE_NAME
APP_NAME
SIZE_OF_APP_VERSION
APP_VERSION
You now have to send a configuration message:
-
10
: tag SIZE_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
: tag SIZE_OF_PACKAGE_NAME
-
97, 110, 100, 114, 111, 105, 116, 118, 45, 114, 101, 109, 111, 116, 101
: that isandroidtv-remote
-
50
: tag SIZE_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 ]
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
2 messages (and their size) must be sent for each command:
-
82,4,8
: the command tag -
KEY_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
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).
To launch an application:
-
210,5
: the command tag SIZE_WHOLE_MESSAGE
-
10
: tag SIZE_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 ]