是否有 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 文件,我发现了一些选项,即
一些使用
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;
几年前我发现 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) 消息 type
是 11
和 status
是 200
:
{"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) 消息 type
是 20
并且 status
是 200
.
配置(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) 类型为 31
且 status
为 [=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) 类型为 41
且 status
为 [=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();
?>
我的任务是为 android 移动设备创建一个应用程序来控制 Android 电视,最好是在任何应用程序(包括设置)之外的 dashboard/landingpage。 它是通过蓝牙还是 wifi 并不重要,尽管我发现蓝牙是不可能的,因为需要 HID 配置文件,并且该配置文件仅在 API 28 上可用(我需要 [= 的支持50=] 19 起)
Play 商店中的一些应用程序已经具有此功能。大多数通过 Wifi 连接到 Android 电视,并与之配对。
通过分析 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;
几年前我发现 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) 消息 type
是 11
和 status
是 200
:
{"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) 消息 type
是 20
并且 status
是 200
.
配置(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) 类型为 31
且 status
为 [=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) 类型为 41
且 status
为 [=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();
?>