feat: 增加信令自动重连保活与控制端视频录制
- 信令客户端增加心跳保活与指数退避自动重连机制 - 服务端优化传输异常日志级别,客户端断开不再报错 - 被控端信令断开时保持服务运行并自动重连 - Flutter 控制端增加远程视频录制功能,兼容标准 WebRTC 模式切换 - 各端连接按钮增加“连接中”状态防重复点击
This commit is contained in:
@@ -267,12 +267,19 @@ public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBi
|
||||
|
||||
// ---- ScreenCaptureService.ServiceStateListener 实现 ----
|
||||
|
||||
@Override
|
||||
public void onSignalConnected() {
|
||||
runOnUiThread(() -> {
|
||||
// 信令服务器连接/重连成功:恢复运行状态显示。
|
||||
updateUI(true);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSignalDisconnected() {
|
||||
runOnUiThread(() -> {
|
||||
// 信令服务器断开:刷新为已停止状态,并解绑/停止服务释放资源。
|
||||
updateUI(false);
|
||||
stopScreenSharing();
|
||||
// 信令服务器短暂断开:保持服务运行,WebSocketClient 会自动重连。
|
||||
binding.tvStatus.setText("状态: 信令断开,正在重连...");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<string name="connection_accept">接受</string>
|
||||
<string name="connection_reject">拒绝</string>
|
||||
<string name="signal_disconnected_message">与信令服务器断开,屏幕共享已停止</string>
|
||||
<string name="signal_reconnecting_message">与信令服务器断开,正在尝试重连…</string>
|
||||
<string name="controller_disconnected_message">控制端(%s)已断开远程控制</string>
|
||||
<string name="accessibility_service_description">用于远程控制时模拟键盘与触摸输入。开启后,控制端可通过本服务向本机发送返回、主页、音量等系统按键及触摸手势。</string>
|
||||
<string name="accessibility_hint">未使用系统签名:如需在本机模拟键盘/触摸,请在系统设置中开启本应用的无障碍服务。</string>
|
||||
|
||||
Reference in New Issue
Block a user