Feat: TV remote control via D-pad focus navigation using AccessibilityService
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
#include <jni.h>
|
||||
#include <android/log.h>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
@@ -9,7 +10,9 @@
|
||||
#include <cinttypes>
|
||||
#include <cstdio>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netdb.h>
|
||||
@@ -22,13 +25,18 @@
|
||||
#include "ScreenHandler.h"
|
||||
#define XXH_INLINE_ALL
|
||||
#include "common/xxhash.h"
|
||||
#include "common/logger.h"
|
||||
|
||||
#define LOG_TAG "YAMA"
|
||||
#undef Mprintf
|
||||
#define Mprintf(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
|
||||
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
|
||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
||||
|
||||
// 1 = 状态提示显示到电视屏幕;0 = 只写 logcat,不显示到屏幕
|
||||
#define SCREEN_STATUS_ENABLED 0
|
||||
|
||||
extern "C" int signMessage_c(const char* pk, int pkLen, const unsigned char* msg,
|
||||
int msgLen, char* buf, int bufSize);
|
||||
|
||||
// ---- 设备信息 helper(登录时填充,仅调用一次)----
|
||||
|
||||
static int GetCpuCores() {
|
||||
@@ -109,23 +117,31 @@ static std::string JsonGetStr(const std::string& json, const char* key) {
|
||||
|
||||
// 查询 ip-api.com,一次请求同时获取公网 IP 和地理位置(与 Windows 客户端同源)
|
||||
// 在连接线程中同步调用,超时 3 秒
|
||||
static bool FetchGeoInfo(std::string& pubIp, std::string& location) {
|
||||
pubIp.clear(); location.clear();
|
||||
struct GeoResult {
|
||||
std::string pubIp, location;
|
||||
std::atomic<bool> ready{false};
|
||||
};
|
||||
|
||||
static void FetchGeoInfoImpl(std::shared_ptr<GeoResult> out) {
|
||||
struct addrinfo hints = {}, *res = nullptr;
|
||||
hints.ai_family = AF_INET;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
if (getaddrinfo("ip-api.com", "80", &hints, &res) != 0 || !res) return false;
|
||||
|
||||
if (getaddrinfo("ip-api.com", "80", &hints, &res) != 0 || !res) {
|
||||
out->ready.store(true, std::memory_order_release); return;
|
||||
}
|
||||
int fd = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (fd < 0) { freeaddrinfo(res); return false; }
|
||||
if (fd < 0) { freeaddrinfo(res); out->ready.store(true, std::memory_order_release); return; }
|
||||
|
||||
struct timeval tv = {3, 0};
|
||||
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK);
|
||||
connect(fd, res->ai_addr, res->ai_addrlen);
|
||||
fd_set wfds; FD_ZERO(&wfds); FD_SET(fd, &wfds);
|
||||
bool connected = (select(fd + 1, nullptr, &wfds, nullptr, &tv) == 1);
|
||||
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) & ~O_NONBLOCK);
|
||||
|
||||
bool ok = false;
|
||||
if (connect(fd, res->ai_addr, res->ai_addrlen) == 0) {
|
||||
if (connected) {
|
||||
const char* req = "GET /json/?fields=status,query,country,city HTTP/1.0\r\n"
|
||||
"Host: ip-api.com\r\n"
|
||||
"Connection: close\r\n\r\n";
|
||||
@@ -140,19 +156,31 @@ static bool FetchGeoInfo(std::string& pubIp, std::string& location) {
|
||||
if (sep != std::string::npos) {
|
||||
const std::string body = resp.substr(sep + 4);
|
||||
if (JsonGetStr(body, "status") == "success") {
|
||||
pubIp = JsonGetStr(body, "query");
|
||||
out->pubIp = JsonGetStr(body, "query");
|
||||
std::string city = JsonGetStr(body, "city");
|
||||
std::string country = JsonGetStr(body, "country");
|
||||
if (!city.empty() && !country.empty()) location = city + ", " + country;
|
||||
else if (!country.empty()) location = country;
|
||||
else if (!city.empty()) location = city;
|
||||
ok = true;
|
||||
if (!city.empty() && !country.empty()) out->location = city + ", " + country;
|
||||
else if (!country.empty()) out->location = country;
|
||||
else if (!city.empty()) out->location = city;
|
||||
}
|
||||
}
|
||||
}
|
||||
close(fd);
|
||||
freeaddrinfo(res);
|
||||
return ok;
|
||||
out->ready.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
// getaddrinfo 在某些设备上会永久阻塞,用独立线程 + 5 秒轮询超时保护 ConnectionThread
|
||||
static bool FetchGeoInfo(std::string& pubIp, std::string& location) {
|
||||
pubIp.clear(); location.clear();
|
||||
auto result = std::make_shared<GeoResult>();
|
||||
std::thread(FetchGeoInfoImpl, result).detach();
|
||||
for (int i = 0; i < 50 && !result->ready.load(std::memory_order_acquire); i++)
|
||||
usleep(100000); // 100ms × 50 = 5 秒
|
||||
if (!result->ready.load(std::memory_order_acquire)) return false;
|
||||
pubIp = result->pubIp;
|
||||
location = result->location;
|
||||
return !pubIp.empty();
|
||||
}
|
||||
|
||||
// ---- 全局状态 ----
|
||||
@@ -173,6 +201,7 @@ static jmethodID g_getActiveWindowMid = nullptr;
|
||||
static jclass g_captureClass = nullptr;
|
||||
static jmethodID g_requestIdrMid = nullptr;
|
||||
static jmethodID g_forceFirstFrameMid = nullptr;
|
||||
static jmethodID g_statusMid = nullptr; // CaptureService.onNativeStatus(String)
|
||||
|
||||
// 心跳日志限流:每 60 秒最多打一次
|
||||
static std::atomic<uint64_t> g_lastHbLogMs{0};
|
||||
@@ -277,6 +306,29 @@ static void RequestIdrFromJava() { CallCaptureStaticVoid(g_requestIdrMid);
|
||||
// 强制 VirtualDisplay 推一帧(静止屏幕绕过 SurfaceFlinger 空闲优化)
|
||||
static void ForceFirstFrameFromJava() { CallCaptureStaticVoid(g_forceFirstFrameMid); }
|
||||
|
||||
// 向 CaptureService.onNativeStatus() 发送状态 Toast(连接线程诊断用)
|
||||
static void PostStatus(const char* msg) {
|
||||
LOGI("STATUS: %s", msg);
|
||||
#if SCREEN_STATUS_ENABLED
|
||||
if (!g_jvm || !g_captureClass || !g_statusMid) return;
|
||||
JNIEnv* jenv = nullptr;
|
||||
bool attached = false;
|
||||
if (g_jvm->GetEnv((void**)&jenv, JNI_VERSION_1_6) == JNI_EDETACHED) {
|
||||
g_jvm->AttachCurrentThread(&jenv, nullptr);
|
||||
attached = true;
|
||||
}
|
||||
if (jenv) {
|
||||
jstring js = jenv->NewStringUTF(msg);
|
||||
if (js) {
|
||||
jenv->CallStaticVoidMethod(g_captureClass, g_statusMid, js);
|
||||
jenv->DeleteLocalRef(js);
|
||||
}
|
||||
if (jenv->ExceptionOccurred()) jenv->ExceptionClear();
|
||||
}
|
||||
if (attached) g_jvm->DetachCurrentThread();
|
||||
#endif
|
||||
}
|
||||
|
||||
static void ScreenSpyThread()
|
||||
{
|
||||
// 等待 Java 侧 startCapture() 调用 nativeSetScreenSize 设置真实分辨率。
|
||||
@@ -299,32 +351,36 @@ static void ScreenSpyThread()
|
||||
std::unique_ptr<IOCPClient> client;
|
||||
std::unique_ptr<AndroidScreenHandler> handler;
|
||||
try {
|
||||
LOGI("SST[%d]: creating IOCPClient", attempt);
|
||||
client = std::make_unique<IOCPClient>(g_bExit, true);
|
||||
client->EnableSubConnAuth(true, g_myClientID);
|
||||
|
||||
LOGI("SST[%d]: connecting %s:%d", attempt, g_serverIp.c_str(), g_serverPort);
|
||||
if (!client->ConnectServer(g_serverIp.c_str(), g_serverPort)) {
|
||||
LOGI("ScreenSpyThread: connect failed (attempt %d/20), retry 2s", attempt);
|
||||
Sleep(2000);
|
||||
continue;
|
||||
}
|
||||
LOGI("SST[%d]: connected, creating handler w=%d h=%d", attempt, w, h);
|
||||
|
||||
handler = std::make_unique<AndroidScreenHandler>(client.get(), w, h);
|
||||
LOGI("SST[%d]: handler created, inserting to set", attempt);
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_shMutex);
|
||||
g_screenHandlers.insert(handler.get());
|
||||
}
|
||||
|
||||
LOGI("SST[%d]: setManagerCallBack", attempt);
|
||||
client->setManagerCallBack(handler.get(),
|
||||
IOCPManager::DataProcess,
|
||||
IOCPManager::ReconnectProcess);
|
||||
LOGI("SST[%d]: SendBitmapInfo", attempt);
|
||||
handler->SendBitmapInfo();
|
||||
// 子连接刚建立,浏览器 initDecoder 完成后需要关键帧才能开始显示。
|
||||
// ForceFirstFrameFromJava:短暂 resize VirtualDisplay 强制 SurfaceFlinger
|
||||
// 推一帧给编码器——静止屏幕时 SurfaceFlinger 有空闲优化不主动推帧,
|
||||
// 导致 REQUEST_SYNC_FRAME 排队等待,造成 10-60 秒黑屏。
|
||||
// RequestIdrFromJava 确保此帧被编码为 IDR。
|
||||
LOGI("SST[%d]: ForceFirstFrame", attempt);
|
||||
ForceFirstFrameFromJava();
|
||||
LOGI("SST[%d]: RequestIdr", attempt);
|
||||
RequestIdrFromJava();
|
||||
LOGI("SST[%d]: entering wait loop", attempt);
|
||||
|
||||
while (client->IsRunning() && client->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
||||
Sleep(200);
|
||||
@@ -354,24 +410,28 @@ static void ScreenSpyThread()
|
||||
int DataProcess(void* /*user*/, PBYTE szBuffer, ULONG ulLength)
|
||||
{
|
||||
if (!szBuffer || !ulLength) return TRUE;
|
||||
|
||||
if (!ClientAuth::IsCommandAllowed(szBuffer[0]))
|
||||
int allowed = (int)ClientAuth::IsCommandAllowed(szBuffer[0]);
|
||||
if (!allowed) {
|
||||
LOGI("DataProcess cmd=%d len=%lu allowed=%d",
|
||||
(int)(unsigned char)szBuffer[0], (unsigned long)ulLength, allowed);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
switch (szBuffer[0]) {
|
||||
case COMMAND_BYE:
|
||||
LOGI("COMMAND_BYE");
|
||||
PostStatus("BYE from server");
|
||||
g_bExit = S_CLIENT_EXIT;
|
||||
break;
|
||||
|
||||
case CMD_HEARTBEAT_ACK:
|
||||
if (ulLength >= 1 + (ULONG)sizeof(HeartbeatACK)) {
|
||||
HeartbeatACK* ack = (HeartbeatACK*)(szBuffer + 1);
|
||||
HeartbeatACK ack;
|
||||
memcpy(&ack, szBuffer + 1, sizeof(HeartbeatACK));
|
||||
uint64_t now = GetUnixMs();
|
||||
g_lastHeartbeatAckMs.store(now, std::memory_order_relaxed);
|
||||
int64_t rtt = (int64_t)now - (int64_t)ack->Time;
|
||||
if (ack->ProcessingMs > 0 && (int64_t)ack->ProcessingMs < rtt)
|
||||
rtt -= ack->ProcessingMs;
|
||||
int64_t rtt = (int64_t)now - (int64_t)ack.Time;
|
||||
if (ack.ProcessingMs > 0 && (int64_t)ack.ProcessingMs < rtt)
|
||||
rtt -= ack.ProcessingMs;
|
||||
g_rttEstimator.update_from_sample((double)rtt);
|
||||
if (now - g_lastHbLogMs.load(std::memory_order_relaxed) >= HB_LOG_INTERVAL_MS) {
|
||||
g_lastHbLogMs.store(now, std::memory_order_relaxed);
|
||||
@@ -386,6 +446,9 @@ int DataProcess(void* /*user*/, PBYTE szBuffer, ULONG ulLength)
|
||||
if (settings.ReportInterval > 0)
|
||||
g_heartbeatInterval = std::max(settings.ReportInterval, 30);
|
||||
LOGI("MasterSettings OK interval=%ds (server=%d)", g_heartbeatInterval, settings.ReportInterval);
|
||||
PostStatus("masterSettings: OK");
|
||||
} else {
|
||||
PostStatus("masterSettings: FAIL");
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -394,7 +457,8 @@ int DataProcess(void* /*user*/, PBYTE szBuffer, ULONG ulLength)
|
||||
// 每个 COMMAND_SCREEN_SPY 对应一个独立子连接,支持多人同时观看/控制
|
||||
size_t active;
|
||||
{ std::lock_guard<std::mutex> lk(g_shMutex); active = g_screenHandlers.size(); }
|
||||
LOGI("COMMAND_SCREEN_SPY → ScreenSpyThread (active=%zu)", active);
|
||||
LOGI("cmd: COMMAND_SCREEN_SPY len=%lu active=%zu w=%d h=%d",
|
||||
(unsigned long)ulLength, active, g_screenWidth.load(), g_screenHeight.load());
|
||||
std::thread(ScreenSpyThread).detach();
|
||||
break;
|
||||
}
|
||||
@@ -421,7 +485,7 @@ int DataProcess(void* /*user*/, PBYTE szBuffer, ULONG ulLength)
|
||||
break;
|
||||
|
||||
default:
|
||||
LOGI("Unhandled cmd=%d", (int)szBuffer[0]);
|
||||
LOGI("cmd: unhandled cmd=%d len=%lu", (int)szBuffer[0], (unsigned long)ulLength);
|
||||
break;
|
||||
}
|
||||
return TRUE;
|
||||
@@ -432,11 +496,10 @@ static void ConnectionThread()
|
||||
{
|
||||
LOGI("ConnectionThread → %s:%d", g_serverIp.c_str(), g_serverPort);
|
||||
|
||||
PostStatus("geo: fetching...");
|
||||
std::string g_pubIp, g_location;
|
||||
FetchGeoInfo(g_pubIp, g_location);
|
||||
// 失败时留空串——服务端检测到空串会回落到自己的 IP 地理查询;
|
||||
// 发 "?" 会被服务端当作有效值缓存,导致永远显示问号
|
||||
LOGI("GeoInfo: ip=%s loc=%s", g_pubIp.c_str(), g_location.c_str());
|
||||
PostStatus(("geo: " + (g_pubIp.empty() ? "failed" : g_pubIp)).c_str());
|
||||
|
||||
LOGIN_INFOR logInfo;
|
||||
strncpy(logInfo.szPCName, g_deviceModel.c_str(), sizeof(logInfo.szPCName) - 1);
|
||||
@@ -474,14 +537,21 @@ static void ConnectionThread()
|
||||
std::unique_ptr<IOCPClient> client(new IOCPClient(g_bExit, false));
|
||||
client->setManagerCallBack(nullptr, DataProcess, nullptr);
|
||||
|
||||
int g_connAttempt = 0;
|
||||
while (S_CLIENT_NORMAL == g_bExit && g_running.load()) {
|
||||
char connMsg[64];
|
||||
snprintf(connMsg, sizeof(connMsg), "connect #%d → %s:%d",
|
||||
++g_connAttempt, g_serverIp.c_str(), g_serverPort);
|
||||
PostStatus(connMsg);
|
||||
clock_t c = clock();
|
||||
if (!client->ConnectServer(g_serverIp.c_str(), g_serverPort)) {
|
||||
LOGI("Connect failed, retry 5s");
|
||||
snprintf(connMsg, sizeof(connMsg), "connect #%d failed errno=%d", g_connAttempt, errno);
|
||||
PostStatus(connMsg);
|
||||
Sleep(5000);
|
||||
continue;
|
||||
}
|
||||
|
||||
PostStatus("connected! sending login...");
|
||||
ClientAuth::OnNewConnection();
|
||||
client->SendLoginInfo(logInfo.Speed(clock() - c));
|
||||
g_lastHeartbeatAckMs.store(GetUnixMs(), std::memory_order_relaxed);
|
||||
@@ -493,16 +563,22 @@ static void ConnectionThread()
|
||||
int interval = g_heartbeatInterval > 0 ? g_heartbeatInterval : 30;
|
||||
for (int i = 0; i < interval; ++i) {
|
||||
if (!client->IsRunning() || !client->IsConnected()
|
||||
|| g_bExit != S_CLIENT_NORMAL || !g_running.load()) break;
|
||||
|| g_bExit != S_CLIENT_NORMAL || !g_running.load()) {
|
||||
char dbg[96];
|
||||
snprintf(dbg, sizeof(dbg), "drop@%ds run=%d conn=%d exit=%d run2=%d",
|
||||
i, (int)client->IsRunning(), (int)client->IsConnected(),
|
||||
(int)(g_bExit == S_CLIENT_NORMAL), (int)g_running.load());
|
||||
PostStatus(dbg);
|
||||
break;
|
||||
}
|
||||
Sleep(1000);
|
||||
}
|
||||
if (!client->IsRunning() || !client->IsConnected()
|
||||
|| g_bExit != S_CLIENT_NORMAL || !g_running.load()) break;
|
||||
|
||||
if (ClientAuth::IsTimedOut()) {
|
||||
LOGI("MasterSettings timeout → reconnect");
|
||||
client->Disconnect();
|
||||
break;
|
||||
PostStatus("timeout: masterSettings");
|
||||
continue;
|
||||
}
|
||||
|
||||
{
|
||||
@@ -510,9 +586,8 @@ static void ConnectionThread()
|
||||
uint64_t last = g_lastHeartbeatAckMs.load(std::memory_order_relaxed);
|
||||
uint64_t now = GetUnixMs();
|
||||
if (last > 0 && now > last && now - last > (uint64_t)ackTO * 1000ULL) {
|
||||
LOGI("ACK timeout → reconnect");
|
||||
client->Disconnect();
|
||||
break;
|
||||
PostStatus("timeout: ACK");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,7 +609,10 @@ static void ConnectionThread()
|
||||
}
|
||||
}
|
||||
}
|
||||
LOGI("Disconnected");
|
||||
PostStatus("disconnected, retry...");
|
||||
// Give server 2 s to remove old context from HostList; without this
|
||||
// the immediate reconnect hits "already exists" and server skips SendMasterSettings.
|
||||
std::this_thread::sleep_for(std::chrono::seconds(2));
|
||||
}
|
||||
|
||||
g_running.store(false);
|
||||
@@ -603,6 +681,10 @@ Java_com_yama_client_YamaBridge_nativeInit(
|
||||
g_forceFirstFrameMid = env->GetStaticMethodID(g_captureClass, "forceFirstFrame", "()V");
|
||||
if (env->ExceptionCheck()) { env->ExceptionClear(); g_forceFirstFrameMid = nullptr; }
|
||||
if (!g_forceFirstFrameMid) LOGE("CaptureService.forceFirstFrame not found");
|
||||
g_statusMid = env->GetStaticMethodID(g_captureClass, "onNativeStatus", "(Ljava/lang/String;)V");
|
||||
if (env->ExceptionCheck()) { env->ExceptionClear(); g_statusMid = nullptr; }
|
||||
if (!g_statusMid) LOGE("CaptureService.onNativeStatus not found");
|
||||
UseAndroidLog(PostStatus);
|
||||
} else {
|
||||
LOGE("CaptureService class not found");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user