From e831ef2795535deb8f937ab5c61d972a70325448 Mon Sep 17 00:00:00 2001 From: TongTongStudio Date: Thu, 30 Jul 2026 00:40:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E4=BF=A1=E4=BB=A4?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E9=87=8D=E8=BF=9E=E4=BF=9D=E6=B4=BB=E4=B8=8E?= =?UTF-8?q?=E6=8E=A7=E5=88=B6=E7=AB=AF=E8=A7=86=E9=A2=91=E5=BD=95=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 信令客户端增加心跳保活与指数退避自动重连机制 - 服务端优化传输异常日志级别,客户端断开不再报错 - 被控端信令断开时保持服务运行并自动重连 - Flutter 控制端增加远程视频录制功能,兼容标准 WebRTC 模式切换 - 各端连接按钮增加“连接中”状态防重复点击 --- .../activity/main/MainActivity.java | 13 +- .../service/ScreenCaptureService.java | 19 ++- .../controlled/signaling/WebSocketClient.java | 126 ++++++++++++++-- .../app/src/main/res/values/strings.xml | 1 + .../src/components/ConnectionPanel.vue | 6 +- .../src/components/StatsBar.vue | 30 ++++ .../src/store/controllerStore.js | 10 +- .../handler/SignalWebSocketHandler.java | 17 ++- .../android/app/src/main/AndroidManifest.xml | 9 ++ .../lib/controller/remote_controller.dart | 1 + webrtc_controller_flutter/lib/main.dart | 139 +++++++++++++++++- .../lib/webrtc/video_recorder.dart | 95 ++++++++++++ webrtc_controller_flutter/pubspec.lock | 2 +- webrtc_controller_flutter/pubspec.yaml | 3 + 14 files changed, 444 insertions(+), 27 deletions(-) create mode 100644 webrtc_controller_flutter/lib/webrtc/video_recorder.dart diff --git a/WebRTCControlled/app/src/main/java/com/ttstd/controlled/activity/main/MainActivity.java b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/activity/main/MainActivity.java index 80012bd..b845c3e 100644 --- a/WebRTCControlled/app/src/main/java/com/ttstd/controlled/activity/main/MainActivity.java +++ b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/activity/main/MainActivity.java @@ -267,12 +267,19 @@ public class MainActivity extends BaseMvvmActivity { + // 信令服务器连接/重连成功:恢复运行状态显示。 + updateUI(true); + }); + } + @Override public void onSignalDisconnected() { runOnUiThread(() -> { - // 信令服务器断开:刷新为已停止状态,并解绑/停止服务释放资源。 - updateUI(false); - stopScreenSharing(); + // 信令服务器短暂断开:保持服务运行,WebSocketClient 会自动重连。 + binding.tvStatus.setText("状态: 信令断开,正在重连..."); }); } diff --git a/WebRTCControlled/app/src/main/java/com/ttstd/controlled/service/ScreenCaptureService.java b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/service/ScreenCaptureService.java index 58262ba..3fbf635 100644 --- a/WebRTCControlled/app/src/main/java/com/ttstd/controlled/service/ScreenCaptureService.java +++ b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/service/ScreenCaptureService.java @@ -544,7 +544,12 @@ public class ScreenCaptureService extends Service { */ public interface ServiceStateListener { /** - * 信令服务器断开(WebSocket 断开)。 + * 信令服务器已连接(首次连接或自动重连成功)。 + */ + void onSignalConnected(); + + /** + * 信令服务器断开(WebSocket 断开,客户端会自动重连)。 */ void onSignalDisconnected(); @@ -616,6 +621,11 @@ public class ScreenCaptureService extends Service { @Override public void onConnected() { Log.i(TAG, "Connected to signal server"); + isShuttingDown = false; // 重连成功后重新武装 + updateNotification("远程控制服务正在运行"); + if (stateListener != null) { + stateListener.onSignalConnected(); + } } @Override @@ -837,9 +847,10 @@ public class ScreenCaptureService extends Service { */ private void onSignalServerDisconnected() { if (isShuttingDown) return; - isShuttingDown = true; - Log.i(TAG, "Signal server disconnected, stopping screen sharing"); - showToast(getString(R.string.signal_disconnected_message)); + Log.i(TAG, "Signal server disconnected, will auto-reconnect"); + // 不再直接停止服务:WebSocketClient 会自动重连,服务保持运行。 + showToast(getString(R.string.signal_reconnecting_message)); + updateNotification("信令断开,正在重连..."); if (stateListener != null) { stateListener.onSignalDisconnected(); } diff --git a/WebRTCControlled/app/src/main/java/com/ttstd/controlled/signaling/WebSocketClient.java b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/signaling/WebSocketClient.java index b932cdf..21057e4 100644 --- a/WebRTCControlled/app/src/main/java/com/ttstd/controlled/signaling/WebSocketClient.java +++ b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/signaling/WebSocketClient.java @@ -7,6 +7,11 @@ import android.util.Log; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; @@ -17,14 +22,29 @@ public class WebSocketClient { private static final String TAG = "WebSocketClient"; + // 应用层心跳:定期发送 PING,避免经过反向代理(nginx 等)空闲超时导致连接被断开。 + private static final long HEARTBEAT_INTERVAL_MS = 25_000; + // OkHttp 协议层 ping:保持 TCP 通道活跃,与心跳互补。 + private static final long PING_INTERVAL_MS = 20_000; + // 自动重连指数退避参数。 + private static final long RECONNECT_BASE_DELAY_MS = 2_000; + private static final long RECONNECT_MAX_DELAY_MS = 30_000; + private static final int RECONNECT_BACKOFF_STEPS = 5; + + private static final String PING_PAYLOAD = "{\"type\":\"PING\"}"; + private final String serverUrl; private final String deviceId; private final Gson gson = new GsonBuilder().disableHtmlEscaping().create(); private final Handler mainHandler = new Handler(Looper.getMainLooper()); private OkHttpClient client; - private WebSocket webSocket; + private volatile WebSocket webSocket; private SignalListener listener; + private ScheduledExecutorService scheduler; + private int reconnectAttempts = 0; + private final AtomicBoolean manuallyClosed = new AtomicBoolean(false); + public interface SignalListener { void onConnected(); void onDisconnected(); @@ -42,14 +62,42 @@ public class WebSocketClient { } public void connect() { - client = new OkHttpClient.Builder().build(); - Request request = new Request.Builder().url(serverUrl).build(); + manuallyClosed.set(false); + reconnectAttempts = 0; + ensureScheduler(); + doConnect(); + } - webSocket = client.newWebSocket(request, new WebSocketListener() { + private void ensureScheduler() { + if (scheduler == null || scheduler.isShutdown()) { + scheduler = Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "WebSocketClient-Scheduler"); + t.setDaemon(true); + return t; + }); + } + } + + private void doConnect() { + if (manuallyClosed.get()) { + return; + } + Log.i(TAG, "Connecting to " + serverUrl); + client = new OkHttpClient.Builder() + .connectTimeout(15, TimeUnit.SECONDS) + .readTimeout(0, TimeUnit.SECONDS) // 不依赖读超时,由心跳/ping 保活 + .writeTimeout(15, TimeUnit.SECONDS) + .pingInterval(PING_INTERVAL_MS, TimeUnit.MILLISECONDS) + .build(); + + Request request = new Request.Builder().url(serverUrl).build(); + client.newWebSocket(request, new WebSocketListener() { @Override public void onOpen(WebSocket ws, Response response) { + webSocket = ws; + reconnectAttempts = 0; Log.i(TAG, "WebSocket connected"); - // 发送注册消息 + startHeartbeat(); registerDevice(); mainHandler.post(() -> { if (listener != null) listener.onConnected(); @@ -72,35 +120,95 @@ public class WebSocketClient { @Override public void onClosed(WebSocket ws, int code, String reason) { Log.i(TAG, "WebSocket closed: " + reason); + webSocket = null; + stopHeartbeat(); mainHandler.post(() -> { if (listener != null) listener.onDisconnected(); }); + scheduleReconnect(); } @Override public void onFailure(WebSocket ws, Throwable t, Response response) { Log.e(TAG, "WebSocket error: " + t.getMessage(), t); + webSocket = null; + stopHeartbeat(); mainHandler.post(() -> { if (listener != null) listener.onError(t.getMessage()); }); + scheduleReconnect(); } }); } + private void startHeartbeat() { + if (scheduler == null || scheduler.isShutdown()) { + return; + } + scheduler.scheduleAtFixedRate(() -> { + WebSocket ws = webSocket; + if (ws != null && !manuallyClosed.get()) { + try { + ws.send(PING_PAYLOAD); + } catch (Exception e) { + Log.w(TAG, "Heartbeat send failed", e); + } + } + }, HEARTBEAT_INTERVAL_MS, HEARTBEAT_INTERVAL_MS, TimeUnit.MILLISECONDS); + } + + private void stopHeartbeat() { + // 心跳任务随 scheduler 关闭或 webSocket 置空而停止,此处无需额外处理。 + } + + private void scheduleReconnect() { + if (manuallyClosed.get()) { + return; + } + if (scheduler == null || scheduler.isShutdown()) { + return; + } + long delay = Math.min( + RECONNECT_BASE_DELAY_MS * (1L << Math.min(reconnectAttempts, RECONNECT_BACKOFF_STEPS)), + RECONNECT_MAX_DELAY_MS); + reconnectAttempts++; + Log.i(TAG, "Scheduling reconnect in " + delay + "ms (attempt " + reconnectAttempts + ")"); + scheduler.schedule(() -> { + if (manuallyClosed.get()) { + return; + } + Log.i(TAG, "Attempting reconnect..."); + doConnect(); + }, delay, TimeUnit.MILLISECONDS); + } + public void sendMessage(SignalMessage message) { - if (webSocket != null) { + WebSocket ws = webSocket; + if (ws != null) { String json = gson.toJson(message); Log.d(TAG, "Sending: " + json); - webSocket.send(json); + ws.send(json); } } public void disconnect() { - if (webSocket != null) { - webSocket.close(1000, "Disconnecting"); + manuallyClosed.set(true); + WebSocket ws = webSocket; + webSocket = null; + stopHeartbeat(); + if (scheduler != null) { + scheduler.shutdownNow(); + scheduler = null; + } + if (ws != null) { + try { + ws.close(1000, "Disconnecting"); + } catch (Exception ignored) { + } } if (client != null) { client.dispatcher().executorService().shutdown(); + client = null; } } diff --git a/WebRTCControlled/app/src/main/res/values/strings.xml b/WebRTCControlled/app/src/main/res/values/strings.xml index aaff217..6fa96f1 100644 --- a/WebRTCControlled/app/src/main/res/values/strings.xml +++ b/WebRTCControlled/app/src/main/res/values/strings.xml @@ -6,6 +6,7 @@ 接受 拒绝 与信令服务器断开,屏幕共享已停止 + 与信令服务器断开,正在尝试重连… 控制端(%s)已断开远程控制 用于远程控制时模拟键盘与触摸输入。开启后,控制端可通过本服务向本机发送返回、主页、音量等系统按键及触摸手势。 未使用系统签名:如需在本机模拟键盘/触摸,请在系统设置中开启本应用的无障碍服务。 diff --git a/WebRTCControllerWeb/src/components/ConnectionPanel.vue b/WebRTCControllerWeb/src/components/ConnectionPanel.vue index e4fb757..0965c49 100644 --- a/WebRTCControllerWeb/src/components/ConnectionPanel.vue +++ b/WebRTCControllerWeb/src/components/ConnectionPanel.vue @@ -31,8 +31,10 @@ function onConnectDevice() { - - + +
被控端设备 (CONTROLLED)