是否有 API 或 SDK 可以在 Android 电视的智能手机上创建远程控制应用程序

Is there an API or SDK to create a remote control application on SmartPhone for Android TV

我的任务是为 android 移动设备创建一个应用程序来控制 Android 电视,最好是在任何应用程序(包括设置)之外的 dashboard/landingpage。 它是通过蓝牙还是 wifi 并不重要,尽管我发现蓝牙是不可能的,因为需要 HID 配置文件,并且该配置文件仅在 API 28 上可用(我需要 [= 的支持50=] 19 起)

Play 商店中的一些应用程序已经具有此功能。大多数通过 Wifi 连接到 Android 电视,并与之配对。

通过分析 APK 文件,我发现了一些选项,即

几年前我发现 Anymote 协议也可以使用,但那个只能用于 Google 电视,而不是 Android 电视。

我现在面临的问题是 connectSDK 库没有得到维护,并且不包含 Android 电视连接的任何代码。 无法在任何地方找到本机 google 包,不确定它是否包含在特定的 Jar 文件中,或者可能是某些 obscured/hidden 依赖项?

我可以尝试使用 Android TV 创建到特定套接字的连接,例如我知道 ServiceType"_androidtvremote._tcp." 并且端口号是 6466.但我不确定什么是最好的实现方式。

我要寻找的是一些如何解决此问题的指示或想法。也许还有一些参考资料。

所以,我找到了我想要的答案。

如果您是 Google 合作伙伴(并且只有这样),并且拥有具有这些权限的帐户,您只需在 this 位置下载 jar 文件。也可以在那里找到文档,并且存在 Android 和 iOS.

的 SDK

关于如何使用它的信息不多。但是通过查看不同的 类 它可以变得清晰。

2021 年 12 月编辑:我为新协议 v2 创建了一个 new documentation


2021 年 9 月编辑:Google is deploying“Android 电视遥控器”的新版本(来自 v4.x 到 v5),并且此版本与旧版配对系统不兼容。现在有必要保持版本 < 5 以使其工作。


我们花了一些时间来寻找如何连接和控制 Android/Google 电视(通过逆向工程),我在这里分享我们的发现结果。更多recent/updated版本,可以查看this wiki page.

我在PHP开发,所以我将在PHP中分享代码(Java代码可以通过使用[=61反编译一些Android应用程序找到=])


感谢@hubertlejaune的大力帮助。

Android 电视(在本文档中又名 server)应该有 2 个开放端口:6466 和 6467.

要了解更多关于 Android 电视的信息,我们可以输入以下 Linux 命令:

openssl s_client -connect SERVER_IP:6467 -prexit -state -debug

这将return一些信息,包括服务器的public证书

如果您只想要服务器的 public 证书:

openssl s_client -showcerts -connect SERVER_IP:6467 </dev/null 2>/dev/null|openssl x509 -outform PEM > server.pem

正在配对

配对协议将在端口 6467 上发生。

客户证书

需要生成我们自己的(客户端)证书。

在PHP中我们可以用下面的代码来完成:

<?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" => "example@google.com"
);

// 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;

它将生成一个名为 client.pem 的文件,其中包含 public 和我们客户端的私钥。

连接到服务器

您需要使用端口 6467 打开与服务器的 TLS/SSL 连接。

在 PHP 中,您可以使用 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();
?>

协议

⚠️ 注意,每条消息都是作为一个JSON字符串发送的,但是有两个components/parts:

  • (首先)我们在 4 个字节上发送消息的长度(JSON 字符串),
  • (第二)我们发送消息(JSON 字符串)本身。

PAIRING_REQUEST(10)

一旦连接到服务器,我们就会发送 PAIRING_REQUEST(10) 消息 (type = 10) .

要发送的第一条消息是:

{"protocol_version":1,"payload":{"service_name":"androidtvremote","client_name":"CLIENT_NAME"},"type":10,"status":200}

服务器 return发送 PAIRING_REQUEST_ACK(11) 消息 type11status200:

{"protocol_version":1,"payload":{},"type":11,"status":200}

选项(20)

然后客户端回复 OPTIONS(20) 消息 (type = 20):

{"protocol_version":1,"payload":{"output_encodings":[{"symbol_length":4,"type":3}],"input_encodings":[{"symbol_length":4,"type":3}],"preferred_role":1},"type":20,"status":200}

服务器 return 的 OPTIONS(20) 消息 type20 并且 status200.

配置(30)

然后客户端回复 CONFIGURATION(30) 消息 (type = 30):

{"protocol_version":1,"payload":{"encoding":{"symbol_length":4,"type":3},"client_role":1},"type":30,"status":200}

服务器 return 发送 CONFIGURATION_ACK(31) 类型为 31status 为 [=21] 的消息=].

代码出现在电视屏幕上!

秘密(40)

然后客户端回复 SECRET(40) 消息 (type = 40):

{"protocol_version":1,"payload":{"secret":"encodedSecret"},"type":40,"status":200}

在这个阶段,电视屏幕显示一个有 4 个字符的代码(例如 4D35)。

找到encodedSecret:

  • 我们使用 SHA-256 哈希;
  • 我们将客户端 public 密钥的 modulus 添加到散列;
  • 我们将客户端 public 密钥的 exponent 添加到散列;
  • 我们将服务器 public 密钥的 modulus 添加到散列;
  • 我们将服务器 public 密钥的 exponent 添加到散列;
  • 我们将代码的最后 2 个字符添加到散列中(在示例中为 35)。

散列的结果然后用 base64 编码。

服务器 return 发送 SECRET_ACK(41) 类型为 41status 为 [=21] 的消息=],以及允许验证的编码秘密——我们没有尝试对其进行解码,但它可能是代码的前 2 个字符:

{"protocol_version":1,"payload":{"secret":"encodedSecretAck"},"type":41,"status":200}

PHP代码

(您可以找到 some Java code 产生的结果几乎相同)

这里是相关的PHP代码:

<?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);

// get the server's public certificate
exec("openssl s_client -showcerts -connect ".escapeshellcmd($host).":6467 </dev/null 2>/dev/null|openssl x509 -outform PEM > server.pem");

$connector = new SecureConnector($dnsConnector, $loop, array(
  'allow_self_signed' => true,
  'verify_peer' => false,
  'verify_peer_name' => false,
  'dns' => false,
  'local_cert' => 'client.pem'
));

// return the message's length on 4 bytes
function getLen($len) {
  return chr($len>>24 & 0xFF).chr($len>>16 & 0xFF).chr($len>>8 & 0xFF).chr($len & 0xFF);
}

// connect to the server
$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";

    // the first response from the server is the message's size on 4 bytes (that looks like a char to convert to decimal) – we can ignore it
    // only look at messages longer than 4 bytes
    if ($dataLen > 4) {
      // decode the JSON string
      $res = json_decode($data);
      // check the status is 200
      if ($res->status === 200) {
        // check at which step we are
        switch($res->type) {
          case 11:{
            // message to send:
            // {"protocol_version":1,"payload":{"output_encodings":[{"symbol_length":4,"type":3}],"input_encodings":[{"symbol_length":4,"type":3}],"preferred_role":1},"type":20,"status":200}
            $json = new stdClass();
            $json->protocol_version = 1;
            $json->payload = new stdClass();
            $json->payload->output_encodings = [];
            $encoding = new stdClass();
            $encoding->symbol_length = 4;
            $encoding->type = 3;
            array_push($json->payload->output_encodings, $encoding);
            $json->payload->input_encodings = [];
            $encoding = new stdClass();
            $encoding->symbol_length = 4;
            $encoding->type = 3;
            array_push($json->payload->input_encodings, $encoding);
            $json->payload->preferred_role = 1;
            $json->type = 20;
            $json->status = 200;
            $payload = json_encode($json);
            $payloadLen = strlen($payload);
            $connection->write(getLen($payloadLen));
            $connection->write($payload);
            break;
          }
          case 20:{
            // message to send:
            // {"protocol_version":1,"payload":{"encoding":{"symbol_length":4,"type":3},"client_role":1},"type":30,"status":200}
            $json = new stdClass();
            $json->protocol_version = 1;
            $json->payload = new stdClass();
            $json->payload->encoding = new stdClass();
            $json->payload->encoding->symbol_length = 4;
            $json->payload->encoding->type = 3;
            $json->payload->client_role = 1;
            $json->type = 30;
            $json->status = 200;
            $payload = json_encode($json);
            $payloadLen = strlen($payload);
            $connection->write(getLen($payloadLen));
            $connection->write($payload);
            break;
          }
          case 31:{
            // when we arrive here, the TV screen displays a code with 4 characters
            // message to send:
            // {"protocol_version":1,"payload":{"secret":"encodedSecret"},"type":40,"status":200}
            $json = new stdClass();
            $json->protocol_version = 1;
            $json->payload = new stdClass();
            // get the code... here we'll let the user to enter it in the console
            $code = readline("Code: ");

            // get the client's certificate
            $clientPub = openssl_get_publickey(file_get_contents("client.pem"));
            $clientPubDetails = openssl_pkey_get_details($clientPub);
            // get the server's certificate
            $serverPub = openssl_get_publickey(file_get_contents("public.key"));
            $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 two characters of the code
            $codeBin = hex2bin(substr($code, 2));
            hash_update($ctxHash, $codeBin);
            $alpha = hash_final($ctxHash, true);
            
            // encode in base64
            $json->payload->secret = base64_encode($alpha);
            $json->type = 40;
            $json->status = 200;
            $payload = json_encode($json);
            $payloadLen = strlen($payload);

            $connection->write(getLen($payloadLen));
            $connection->write($payload);
            break;
          }
        }
      }
    }
  });

  // send the first message to the server
  // {"protocol_version":1,"payload":{"service_name":"androidtvremote","client_name":"TEST"},"type":10,"status":200}
  $json = new stdClass();
  $json->protocol_version = 1;
  $json->payload = new stdClass();
  $json->payload->service_name = "androidtvremote";
  $json->payload->client_name = "interface Web";
  $json->type = 10;
  $json->status = 200;
  $payload = json_encode($json);
  $payloadLen = strlen($payload);

  // send the message size
  $connection->write(getLen($payloadLen));
  // send the message
  $connection->write($payload);
}, 'printf');

$loop->run();
?>

发送命令

现在客户端已与服务器配对,我们将使用端口 6466 发送命令。
请注意,我们将为命令使用一个字节数组。

配置信息

必须发送初始消息:

[1,0,0,21,0,0,0,1,0,0,0,1,32,3,0,0,0,0,0,0,4,116,101,115,116]

服务器将响应以 [1,7,0

开头的字节数组

命令

您必须发送两条消息才能执行一条命令。

格式为:

[1,2,0,{SIZE=16},0,0,0,0,0,0,0, {COUNTER} ,0,0,0, {PRESS=0} ,0,0,0,{KEYCODE}]
[1,2,0,{SIZE=16},0,0,0,0,0,0,0,{COUNTER+1},0,0,0,{RELEASE=1},0,0,0,{KEYCODE}]

可以在 https://developer.android.com/reference/android/view/KeyEvent 上找到 {KEYCODE}

例如,如果我们要发送一个VOLUME_UP:

[1,2,0,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24]
[1,2,0,16,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,24]

PHP代码

还有一些 PHP 代码:

<?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'
));

// convert the array of bytes
function toMsg($arr) {
  $chars = array_map("chr", $arr);
  return join($chars);
}

// connect to the server
$connector->connect('tls://' . $host . ':6466')->then(function (ConnectionInterface $connection) use ($host) {
  $connection->on('data', function ($data) use ($connection) {
    // convert the data received to an array of bytes
    $dataLen = strlen($data);
    $arr = [];
    for ($i=0; $i<$dataLen;$i++) {
      $arr[] = ord($data[$i]);
    }
    $str = "[".implode(",", $arr)."]";
    echo "data recv => ".$data." ".$str." (".strlen($data).")\n";

    // if we receive [1,20,0,0] it means the server sent a ping
    if (strpos($str, "[1,20,0,0]") === 0) {
      // we can reply with a PONG [1,21,0,0] if we want
      // $connection->write(toMsg([1,21,0,0]));
    }
    else if (strpos($str, "[1,7,0,") === 0) {
      // we can send the command, here it's a VOLUME_UP
      $connection->write(toMsg([1,2,0,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24]));
      $connection->write(toMsg([1,2,0,16,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,24]));
    }
  });

  // send the first message (configuration) to the server
  $arr = [1,0,0,21,0,0,0,1,0,0,0,1,32,3,0,0,0,0,0,0,4,116,101,115,116];
  $connection->write(toMsg($arr));
}, 'printf');

$loop->run();
?>