是否有 API 或 SDK 可以在 SmartPhone for Android TV 上创建远程控制应用程序

Chr*_*lla 6 android remote-control

我的任务是为 android mobile 创建一个应用程序来控制 Android TV,最好是任何应用程序之外的仪表板/登陆页面(包括设置)。是通过蓝牙还是 wifi 并不重要,尽管我发现蓝牙是不可能的,因为需要 HID 配置文件,并且该配置文件仅在 API 28 上可用(我需要从 API 19 开始支持)

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

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

  • 有些使用 connectSDK

  • 其他人使用的似乎是我似乎找不到的本地 google 包

    import com.google.android.tv.support.remote.Discovery;
    import com.google.android.tv.support.remote.core.Client;
    import com.google.android.tv.remote.BuildInfo;
    
    Run Code Online (Sandbox Code Playgroud)

我发现几年前也可以使用 Anymote 协议,但该协议仅适用于 Google TV,不适用于 Android TV。

我现在面临的问题是connectSDK库没有得到维护并且不包含任何用于 Android TV 连接的代码。本地 google 包在任何地方都找不到,不确定它是否包含在特定的 Jar 文件中,或者可能是一些模糊/隐藏的依赖项?

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

我正在寻找的是我如何解决这个问题的一些指示或想法。也许也有一些参考。

Aym*_*Kdn 15

EDIT on December 2021: I created a new documentation for the new protocol v2.

\n
\n

EDIT on September 2021: Google is deploying a new version of the "Android TV Remote Control" (from v4.x to v5), and this version is not compatible with the legacy pairing system. For now it\'s necessary to keep a version < 5 to make it work.

\n
\n

We spent some time to find how to connect and control an Android/Google TV (by reverse engineering), and I\'m sharing here the result of our findings. For a more recent/updated version, you can check this wiki page.

\n

I develop in PHP so I\'ll share the code in PHP (the Java code can be found by decompiling some Android apps using https://github.com/skylot/jadx)

\n
\n

Thanks to @hubertlejaune for his tremendous help.

\n

The Android TV (aka server in this document) should have 2 open ports: 6466 and 6467.

\n

To know more about the Android TV, we can enter the below Linux command:

\n
\n

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

\n
\n

Which will return some information, including the server\'s public certificate.

\n

If you only want the server\'s public certificate:

\n
\n

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

\n
\n

Pairing

\n

The pairing protocol will happen on port 6467.

\n

Client\'s certificate

\n

It\'s required to generate our own (client) certificate.

\n

In PHP we can do it with the below code:

\n
<?php\n// the commande line is: php generate_key.php > client.pem\n\n// certificate details (Distinguished Name)\n// (OpenSSL applies defaults to missing fields)\n$dn = array(\n  "commonName" => "atvremote",\n  "countryName" => "US",\n  "stateOrProvinceName" => "California",\n  "localityName" => "Montain View",\n  "organizationName" => "Google Inc.",\n  "organizationalUnitName" => "Android",\n  "emailAddress" => "example@google.com"\n);\n\n// create certificate which is valid for ~10 years\n$privkey = openssl_pkey_new();\n$cert = openssl_csr_new($dn, $privkey);\n$cert = openssl_csr_sign($cert, null, $privkey, 3650);\n\n// export public key\nopenssl_x509_export($cert, $out);\necho $out;\n\n// export private key\n$passphrase = null;\nopenssl_pkey_export($privkey, $out, $passphrase);\necho $out;\n
Run Code Online (Sandbox Code Playgroud)\n

It will generate a file called client.pem that contains both the public and the private keys for our client.

\n

Connection to the server

\n

You need to open a TLS/SSL connection to the server using port 6467.

\n

In PHP, you could use https://github.com/reactphp/socket:

\n
<?php\nuse React\\EventLoop\\Factory;\nuse React\\Socket\\Connector;\nuse React\\Socket\\SecureConnector;\nuse React\\Socket\\ConnectionInterface;\n\nrequire __DIR__ . \'/./vendor/autoload.php\';\n\n$host = \'SERVER_IP\';\n$loop = Factory::create();\n$tcpConnector = new React\\Socket\\TcpConnector($loop);\n$dnsResolverFactory = new React\\Dns\\Resolver\\Factory();\n$dns = $dnsResolverFactory->createCached(\'8.8.8.8\', $loop);\n$dnsConnector = new React\\Socket\\DnsConnector($tcpConnector, $dns);\n\n$connector = new SecureConnector($dnsConnector, $loop, array(\n  \'allow_self_signed\' => true,\n  \'verify_peer\' => false,\n  \'verify_peer_name\' => false,\n  \'dns\' => false,\n  \'local_cert\' => \'client.pem\'\n));\n\n$connector->connect(\'tls://\' . $host . \':6467\')->then(function (ConnectionInterface $connection) use ($host) {\n  $connection->on(\'data\', function ($data) use ($connection) {\n    $dataLen = strlen($data);\n    echo "data recv => ".$data." (".strlen($data).")\\n";\n    // deal with the messages received from the server\n  });\n  \n  // below we can send the first message\n  $connection->write(/* first message here */);\n}, \'printf\');\n\n$loop->run();\n?>\n
Run Code Online (Sandbox Code Playgroud)\n

Protocol

\n

\xe2\x9a\xa0\xef\xb8\x8f Attention, each message is sent as a JSON string, but with two components/parts:

\n
    \n
  • (first) we send the length of the message (JSON string) on 4 bytes,
  • \n
  • (second) we send the message (JSON string) itself.
  • \n
\n

PAIRING_REQUEST(10)

\n

As soon as we are connected to the server, we send a PAIRING_REQUEST(10) message (type = 10).

\n

The first message to send is:

\n
\n

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

\n
\n

The server returns a PAIRING_REQUEST_ACK(11) message with type is 11 and status is 200:

\n
\n

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

\n
\n

OPTIONS(20)

\n

Then the client replies with a OPTIONS(20) message (type = 20):

\n
\n

{"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}

\n
\n

The server returns a OPTIONS(20) message with type is 20 and status is 200.

\n

CONFIGURATION(30)

\n

Then the client replies with a CONFIGURATION(30) message (type = 30):

\n
\n

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

\n
\n

The server returns a CONFIGURATION_ACK(31) message with type is 31 and status is 200.

\n

代码出现在电视屏幕上!

\n

秘密(40)

\n

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

\n
\n

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

\n
\n

此时,电视屏幕上会显示4个字符的代码(例如4D35)。

\n

为了找到encodedSecret:

\n
    \n
  • 我们使用 SHA-256 哈希;
  • \n
  • 我们添加客户端公钥modulus to the hash;
  • \n
  • 我们添加客户端公钥exponent to the hash;
  • \n
  • 我们添加服务器公钥modulus to the hash;
  • \n
  • 我们添加服务器公钥exponent to the hash;
  • \n
  • 我们将代码的最后 2 个字符添加到哈希中(在示例中它是35).
  • \n
\n

然后将哈希结果编码为 Base64。

\n

服务器返回类型为 is和is的SECRET_ACK(41)消息,以及允许验证 \xe2\x80\x93 的编码秘密,我们没有尝试对其进行解码,但它可能是前 2 个代码的字符:41status200

\n
\n

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

\n
\n

PHP代码

\n

(你可以找到一些产生几乎相同结果的Java 代码)

\n

下面是相关的PHP代码:

\n
<?php\nuse React\\EventLoop\\Factory;\nuse React\\Socket\\Connector;\nuse React\\Socket\\SecureConnector;\nuse React\\Socket\\ConnectionInterface;\n\nrequire __DIR__ . \'/./vendor/autoload.php\';\n\n$host = \'SERVER_IP\';\n$loop = Factory::create();\n$tcpConnector = new React\\Socket\\TcpConnector($loop);\n$dnsResolverFactory = new React\\Dns\\Resolver\\Factory();\n$dns = $dnsResolverFactory->createCached(\'8.8.8.8\', $loop);\n$dnsConnector = new React\\Socket\\DnsConnector($tcpConnector, $dns);\n\n// get the server\'s public certificate\nexec("openssl s_client -showcerts -connect ".escapeshellcmd($host).":6467 </dev/null 2>/dev/null|openssl x509 -outform PEM > server.pem");\n\n$connector = new SecureConnector($dnsConnector, $loop, array(\n  \'allow_self_signed\' => true,\n  \'verify_peer\' => false,\n  \'verify_peer_name\' => false,\n  \'dns\' => false,\n  \'local_cert\' => \'client.pem\'\n));\n\n// return the message\'s length on 4 bytes\nfunction getLen($len) {\n  return chr($len>>24 & 0xFF).chr($len>>16 & 0xFF).chr($len>>8 & 0xFF).chr($len & 0xFF);\n}\n\n// connect to the server\n$connector->connect(\'tls://\' . $host . \':6467\')->then(function (ConnectionInterface $connection) use ($host) {\n  $connection->on(\'data\', function ($data) use ($connection) {\n    $dataLen = strlen($data);\n    echo "data recv => ".$data." (".strlen($data).")\\n";\n\n    // the first response from the server is the message\'s size on 4 bytes (that looks like a char to convert to decimal) \xe2\x80\x93 we can ignore it\n    // only look at messages longer than 4 bytes\n    if ($dataLen > 4) {\n      // decode the JSON string\n      $res = json_decode($data);\n      // check the status is 200\n      if ($res->status === 200) {\n        // check at which step we are\n        switch($res->type) {\n          case 11:{\n            // message to send:\n            // {"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}\n            $json = new stdClass();\n            $json->protocol_version = 1;\n            $json->payload = new stdClass();\n            $json->payload->output_encodings = [];\n            $encoding = new stdClass();\n            $encoding->symbol_length = 4;\n            $encoding->type = 3;\n            array_push($json->payload->output_encodings, $encoding);\n            $json->payload->input_encodings = [];\n            $encoding = new stdClass();\n            $encoding->symbol_length = 4;\n            $encoding->type = 3;\n            array_push($json->payload->input_encodings, $encoding);\n            $json->payload->preferred_role = 1;\n            $json->type = 20;\n            $json->status = 200;\n            $payload = json_encode($json);\n            $payloadLen = strlen($payload);\n            $connection->write(getLen($payloadLen));\n            $connection->write($payload);\n            break;\n          }\n          case 20:{\n            // message to send:\n            // {"protocol_version":1,"payload":{"encoding":{"symbol_length":4,"type":3},"client_role":1},"type":30,"status":200}\n            $json = new stdClass();\n            $json->protocol_version = 1;\n            $json->payload = new stdClass();\n            $json->payload->encoding = new stdClass();\n            $json->payload->encoding->symbol_length = 4;\n            $json->payload->encoding->type = 3;\n            $json->payload->client_role = 1;\n            $json->type = 30;\n            $json->status = 200;\n            $payload = json_encode($json);\n            $payloadLen = strlen($payload);\n            $connection->write(getLen($payloadLen));\n            $connection->write($payload);\n            break;\n          }\n          case 31:{\n            // when we arrive here, the TV screen displays a code with 4 characters\n            // message to send:\n            // {"protocol_version":1,"payload":{"secret":"encodedSecret"},"type":40,"status":200}\n            $json = new stdClass();\n            $json->protocol_version = 1;\n            $json->payload = new stdClass();\n            // get the code... here we\'ll let the user to enter it in the console\n            $code = readline("Code: ");\n\n            // get the client\'s certificate\n            $clientPub = openssl_get_publickey(file_get_contents("client.pem"));\n            $clientPubDetails = openssl_pkey_get_details($clientPub);\n            // get the server\'s certificate\n            $serverPub = openssl_get_publickey(file_get_contents("public.key"));\n            $serverPubDetails = openssl_pkey_get_details($serverPub);\n\n            // get the client\'s certificate modulus\n            $clientModulus = $clientPubDetails[\'rsa\'][\'n\'];\n            // get the client\'s certificate exponent\n            $clientExponent = $clientPubDetails[\'rsa\'][\'e\'];\n            // get the server\'s certificate modulus\n            $serverModulus = $serverPubDetails[\'rsa\'][\'n\'];\n            // get the server\'s certificate exponent\n            $serverExponent = $serverPubDetails[\'rsa\'][\'e\'];\n\n            // use SHA-256\n            $ctxHash = hash_init(\'sha256\');\n            hash_update($ctxHash, $clientModulus);\n            hash_update($ctxHash, $clientExponent);\n            hash_update($ctxHash, $serverModulus);\n            hash_update($ctxHash, $serverExponent);\n            // only keep the last two characters of the code\n            $codeBin = hex2bin(substr($code, 2));\n            hash_update($ctxHash, $codeBin);\n            $alpha = hash_final($ctxHash, true);\n            \n            // encode in base64\n            $json->payload->secret = base64_encode($alpha);\n            $json->type = 40;\n            $json->status = 200;\n            $payload = json_encode($json);\n            $payloadLen = strlen($payload);\n\n            $connection->write(getLen($payloadLen));\n            $connection->write($payload);\n            break;\n          }\n        }\n      }\n    }\n  });\n\n  // send the first message to the server\n  // {"protocol_version":1,"payload":{"service_name":"androidtvremote","client_name":"TEST"},"type":10,"status":200}\n  $json = new stdClass();\n  $json->protocol_version = 1;\n  $json->payload = new stdClass();\n  $json->payload->service_name = "androidtvremote";\n  $json->payload->client_name = "interface Web";\n  $json->type = 10;\n  $json->status = 200;\n  $payload = json_encode($json);\n  $payloadLen = strlen($payload);\n\n  // send the message size\n  $connection->write(getLen($payloadLen));\n  // send the message\n  $connection->write($payload);\n}, \'printf\');\n\n$loop->run();\n?>\n
Run Code Online (Sandbox Code Playgroud)\n

发送命令

\n

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

\n

配置消息

\n

必须发送初始消息:

\n
\n

[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]

\n
\n

服务器将响应一个字节数组,该数组应以[1,7,0

\n

命令

\n

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

\n

格式为:

\n
\n

[1,2,0,{大小=16},0,0,0,0,0,0,0, {计数器} ,0,0,0, {按=0} ,0,0,0,{ KEYCODE}]
\n[1,2,0,{SIZE=16},0,0,0,0,0,0,0,{COUNTER+1},0,0,0,{RELEASE=1}, 0,0,0,{KEYCODE}]

\n
\n

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

\n

例如,如果我们想发送VOLUME_UP

\n
\n

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

\n
\n

PHP代码

\n

这里是一些 PHP 代码:

\n
<?php\nuse React\\EventLoop\\Factory;\nuse React\\Socket\\Connector;\nuse React\\Socket\\SecureConnector;\nuse React\\Socket\\ConnectionInterface;\n\nrequire __DIR__ . \'/./vendor/autoload.php\';\n\n$host = \'SERVER_IP\';\n$loop = Factory::create();\n$tcpConnector = new React\\Socket\\TcpConnector($loop);\n$dnsResolverFactory = new React\\Dns\\Resolver\\Factory();\n$dns = $dnsResolverFactory->createCached(\'8.8.8.8\', $loop);\n$dnsConnector = new React\\Socket\\DnsConnector($tcpConnector, $dns);\n\n$connector = new SecureConnector($dnsConnector, $loop, array(\n  \'allow_self_signed\' => true,\n  \'verify_peer\' => false,\n  \'verify_peer_name\' => false,\n  \'dns\' => false,\n  \'local_cert\' => \'client.pem\'\n));\n\n// convert the array of bytes\nfunction toMsg($arr) {\n  $chars = array_map("chr", $arr);\n  return join($chars);\n}\n\n// connect to the server\n$connector->connect(\'tls://\' . $host . \':6466\')->then(function (ConnectionInterface $connection) use ($host) {\n  $connection->on(\'data\', function ($data) use ($connection) {\n    // convert the data received to an array of bytes\n    $dataLen = strlen($data);\n    $arr = [];\n    for ($i=0; $i<$dataLen;$i++) {\n      $arr[] = ord($data[$i]);\n    }\n    $str = "[".implode(",", $arr)."]";\n    echo "data recv => ".$data." ".$str." (".strlen($data).")\\n";\n\n    // if we receive [1,20,0,0] it means the server sent a ping\n    if (strpos($str, "[1,20,0,0]") === 0) {\n      // we can reply with a PONG [1,21,0,0] if we want\n      // $connection->write(toMsg([1,21,0,0]));\n    }\n    else if (strpos($str, "[1,7,0,") === 0) {\n      // we can send the command, here it\'s a VOLUME_UP\n      $connection->write(toMsg([1,2,0,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24]));\n      $connection->write(toMsg([1,2,0,16,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,24]));\n    }\n  });\n\n  // send the first message (configuration) to the server\n  $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];\n  $connection->write(toMsg($arr));\n}, \'printf\');\n\n$loop->run();\n?>\n
Run Code Online (Sandbox Code Playgroud)\n


Chr*_*lla 4

所以,我找到了我正在寻找的答案。

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

关于如何使用它的信息并不多。但通过查看不同的类,情况就会变得很清楚。