From 3e0e9f32591bd1a4e1e22bd3a5b4a3482a2bb360 Mon Sep 17 00:00:00 2001 From: tongtongstudio Date: Tue, 2 Jun 2026 10:59:18 +0800 Subject: [PATCH] =?UTF-8?q?build:=20=E6=9B=B4=E6=96=B0=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E8=87=B3studio=20panda?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 6 +- .../activity/WebRTCScreenCaptureActivity.java | 183 +++++++- .../activity/main/MainActivity.java | 1 - .../remoteservice/config/WebSocketConfig.java | 18 + .../network/WebSocketCallback.java | 42 ++ .../network/WebSocketManager.java | 413 ++++++++++++++++++ .../remoteservice/utils/SystemUtils.java | 29 ++ .../layout/activity_webrtc_screen_capture.xml | 71 ++- build.gradle | 4 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 10 files changed, 730 insertions(+), 39 deletions(-) create mode 100644 app/src/main/java/com/ttstd/remoteservice/config/WebSocketConfig.java create mode 100644 app/src/main/java/com/ttstd/remoteservice/network/WebSocketCallback.java create mode 100644 app/src/main/java/com/ttstd/remoteservice/network/WebSocketManager.java diff --git a/app/build.gradle b/app/build.gradle index ee0c636..82246a7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,6 +29,10 @@ android { enabled true } + viewBinding { + enabled true + } + javaCompileOptions { annotationProcessorOptions { arguments = [AROUTER_MODULE_NAME: project.getName()] @@ -101,7 +105,7 @@ dependencies { implementation 'com.jeremyliao:live-event-bus-x:1.7.3' implementation 'org.webrtc:google-webrtc:1.0.32006' - implementation 'org.java-websocket:Java-WebSocket:1.5.3' +// implementation 'org.java-websocket:Java-WebSocket:1.5.3' //MMKV implementation 'com.tencent:mmkv-static:1.2.14' diff --git a/app/src/main/java/com/ttstd/remoteservice/activity/WebRTCScreenCaptureActivity.java b/app/src/main/java/com/ttstd/remoteservice/activity/WebRTCScreenCaptureActivity.java index 8576f42..55464d5 100644 --- a/app/src/main/java/com/ttstd/remoteservice/activity/WebRTCScreenCaptureActivity.java +++ b/app/src/main/java/com/ttstd/remoteservice/activity/WebRTCScreenCaptureActivity.java @@ -14,7 +14,9 @@ import android.os.Bundle; import android.util.Log; import android.view.Surface; import android.view.TextureView; +import android.view.View; import android.widget.Button; +import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; @@ -23,7 +25,11 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import com.ttstd.remoteservice.R; +import com.ttstd.remoteservice.network.WebSocketCallback; +import com.ttstd.remoteservice.network.WebSocketManager; import com.ttstd.remoteservice.service.ScreenCaptureService2; import org.webrtc.CapturerObserver; @@ -46,7 +52,9 @@ import org.webrtc.VideoTrack; import java.util.ArrayList; import java.util.List; -public class WebRTCScreenCaptureActivity extends AppCompatActivity { +import okio.ByteString; + +public class WebRTCScreenCaptureActivity extends AppCompatActivity implements WebSocketCallback { private static final String TAG = "WebRTCScreenCaptureActivity"; // 请求码 private static final int REQUEST_CODE_SCREEN_CAPTURE = 1001; @@ -71,7 +79,8 @@ public class WebRTCScreenCaptureActivity extends AppCompatActivity { private int screenDpi; // UI控件 - private Button btnStartPush; + private TextView tvStatus; + private Button btConnect, btnStartPush; private SurfaceViewRenderer localRender; // 本地预览(可选) private TextureView textureView; @@ -82,16 +91,27 @@ public class WebRTCScreenCaptureActivity extends AppCompatActivity { private String remoteSdp = ""; // 替换为远端实际SDP private List remoteIceCandidates = new ArrayList<>(); + private WebSocketManager webSocketManager; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_webrtc_screen_capture); // 初始化控件 + tvStatus = findViewById(R.id.tv_status); + btConnect = findViewById(R.id.bt_connect); btnStartPush = findViewById(R.id.btn_start_push); localRender = findViewById(R.id.local_render); textureView = findViewById(R.id.textureView); + btConnect.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + initWebSocket(); + } + }); + textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { @Override public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) { @@ -173,6 +193,15 @@ public class WebRTCScreenCaptureActivity extends AppCompatActivity { startActivityForResult(captureIntent, REQUEST_CODE_SCREEN_CAPTURE); } + private void initWebSocket() { + Log.e(TAG, "initWebSocket: "); + // 初始化 WebSocket 管理器 + webSocketManager = WebSocketManager.getInstance(); +// String wsUrl = "ws://175.178.213.60:2310/signaling/981964879"; + String wsUrl = "ws://192.168.100.111:2310/signaling/981964879"; + webSocketManager.init(wsUrl, this); + } + /** * 初始化WebRTC核心组件 */ @@ -348,8 +377,8 @@ public class WebRTCScreenCaptureActivity extends AppCompatActivity { // 添加用户指定的 STUN 服务器 PeerConnection.IceServer stunServer = PeerConnection.IceServer.builder("stun:47.242.112.133:3478") - .setUsername("tt") - .setPassword("tongtong") + .setUsername("tt") + .setPassword("tongtong") .createIceServer(); iceServers.add(stunServer); @@ -368,8 +397,139 @@ public class WebRTCScreenCaptureActivity extends AppCompatActivity { return iceServers; } + /** + * 发送SDP给远端信令服务器 + */ private void sendSdpToRemote(SessionDescription sdp) { - // TODO: 实现信令发送逻辑,例如通过WebSocket发送SDP字符串 + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("msg_type", 1); + + JsonObject sdpJson = new JsonObject(); + sdpJson.addProperty("type", sdp.type.canonicalForm()); // "offer" 或 "answer" + sdpJson.addProperty("sdp", sdp.description); + // TODO: 2026/3/11 之后改为唯一标识码 + sdpJson.addProperty("fromUser", "981964879"); // 标识发送方 + sdpJson.addProperty("timestamp", System.currentTimeMillis()); + + jsonObject.add("content", sdpJson); + String json = jsonObject.toString(); + + Log.d(TAG, "发送SDP到信令服务器: "); + Log.e(TAG, "sendSdpToRemote: SDP内容: " + json); + + webSocketManager.sendMessage(json); + } + + /** + * 发送ICE Candidate给远端信令服务器 + */ + private void sendIceCandidateToRemote(IceCandidate iceCandidate) { + Log.d(TAG, "Send ICE Candidate to Remote: " + iceCandidate.sdpMid + " " + iceCandidate.sdpMLineIndex + " " + iceCandidate.sdp); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("msg_type", 2); + + JsonObject iceJson = new JsonObject(); + iceJson.addProperty("sdpMid", iceCandidate.sdpMid); + iceJson.addProperty("sdpMLineIndex", iceCandidate.sdpMLineIndex); + iceJson.addProperty("sdp", iceCandidate.sdp); + + // TODO: 2026/3/11 之后改为唯一标识码 + iceJson.addProperty("fromUser", "981964879"); + iceJson.addProperty("timestamp", System.currentTimeMillis()); + + jsonObject.add("content", iceJson); + + String json = jsonObject.toString(); + + Log.d(TAG, "发送ICE Candidate到信令服务器: "); + Log.e(TAG, "sendIceCandidateToRemote: ICE内容: " + json); + webSocketManager.sendMessage(json); + } + + /** + * 处理信令服务器响应(接收远端的Answer SDP) + */ + private void handleSignalingResponse(String response) { + JsonObject jsonResponse = JsonParser.parseString(response).getAsJsonObject(); + ; + + // 检查是否包含远端的Answer SDP + if (jsonResponse.has("type") && "answer".equals(jsonResponse.get("type").getAsString())) { + String remoteSdp = jsonResponse.get("sdp").getAsString(); + SessionDescription remoteSessionDescription = + new SessionDescription(SessionDescription.Type.ANSWER, remoteSdp); + + // 设置远端SDP + peerConnection.setRemoteDescription(new SimpleSdpObserver(), remoteSessionDescription); + Log.d(TAG, "已设置远端Answer SDP"); + } + + // 检查是否包含远端的ICE Candidate + if (jsonResponse.has("candidate")) { + String sdpMid = jsonResponse.get("sdpMid").getAsString(); + int sdpMLineIndex = jsonResponse.get("sdpMLineIndex").getAsInt(); + String candidate = jsonResponse.get("candidate").getAsString(); + + IceCandidate remoteIceCandidate = new IceCandidate(sdpMid, sdpMLineIndex, candidate); + peerConnection.addIceCandidate(remoteIceCandidate); + Log.d(TAG, "已添加远端ICE Candidate"); + } + + } + + @Override + public void onConnected() { + Log.e(TAG, "onConnected: "); + tvStatus.setText("websocket已连接"); + btConnect.setEnabled(false); + } + + @Override + public void onDisconnected(String reason) { + Log.e(TAG, "onDisconnected: " + reason); + tvStatus.setText("websocket已断开:" + reason); + btConnect.setEnabled(true); + } + + @Override + public void onMessage(String message) { + Log.e(TAG, "onMessage: " + message); + } + + @Override + public void onMessage(ByteString bytes) { + Log.e(TAG, "onMessage: "); + } + + @Override + public void onError(String error) { + Log.e(TAG, "onError: " + error); + tvStatus.setText("websocket错误:" + error); + btConnect.setEnabled(true); + } + + /** + * 简化的SdpObserver实现 + */ + private static class SimpleSdpObserver implements SdpObserver { + @Override + public void onCreateSuccess(SessionDescription sessionDescription) { + } + + @Override + public void onSetSuccess() { + Log.d(TAG, "SDP设置成功"); + } + + @Override + public void onCreateFailure(String error) { + Log.e(TAG, "创建SDP失败: " + error); + } + + @Override + public void onSetFailure(String error) { + Log.e(TAG, "设置SDP失败: " + error); + } } /** @@ -476,15 +636,6 @@ public class WebRTCScreenCaptureActivity extends AppCompatActivity { } } - /** - * 发送ICE候选到远端(示例:实际需通过WebSocket/HTTP信令服务器) - */ - private void sendIceCandidateToRemote(IceCandidate iceCandidate) { - // 这里仅做日志打印,实际需替换为信令发送逻辑 - Log.d(TAG, "Send ICE Candidate to Remote: " + iceCandidate.sdpMid + " " + iceCandidate.sdpMLineIndex + " " + iceCandidate.sdp); - // 示例:将iceCandidate转为JSON发送到远端服务器 - } - /** * 处理权限请求结果 */ @@ -606,6 +757,10 @@ public class WebRTCScreenCaptureActivity extends AppCompatActivity { // rootEglBase.release(); // } + if (webSocketManager != null) { + webSocketManager.disconnect(); + } + // 重置按钮 btnStartPush.setText("开始推流"); btnStartPush.setOnClickListener(v -> checkPermissionsAndStart()); diff --git a/app/src/main/java/com/ttstd/remoteservice/activity/main/MainActivity.java b/app/src/main/java/com/ttstd/remoteservice/activity/main/MainActivity.java index a505c2e..8633492 100644 --- a/app/src/main/java/com/ttstd/remoteservice/activity/main/MainActivity.java +++ b/app/src/main/java/com/ttstd/remoteservice/activity/main/MainActivity.java @@ -24,7 +24,6 @@ public class MainActivity extends BaseMvvmActivity messageQueue = new ConcurrentLinkedQueue<>(); + // 连接状态锁 + private final Object connectionLock = new Object(); + + private WebSocketManager() { + // 私有构造函数 + } + + public static WebSocketManager getInstance() { + if (instance == null) { + synchronized (WebSocketManager.class) { + if (instance == null) { + instance = new WebSocketManager(); + } + } + } + return instance; + } + + /** + * 初始化 WebSocket 连接 + */ + public void init(String url, WebSocketCallback callback) { + this.wsUrl = url; + this.callback = callback; + + client = new OkHttpClient.Builder() + .connectTimeout(WebSocketConfig.connectTimeout, TimeUnit.SECONDS) + .readTimeout(WebSocketConfig.readTimeout, TimeUnit.SECONDS) + .writeTimeout(WebSocketConfig.writeTimeout, TimeUnit.SECONDS) + .pingInterval(10, TimeUnit.SECONDS) // 设置心跳 + .retryOnConnectionFailure(true) + .build(); + + connect(); + } + + /** + * 建立 WebSocket 连接 + */ + private void connect() { + if (wsUrl == null) { + Log.e(TAG, "WebSocket URL 不能为 null"); + return; + } + + if (TextUtils.isEmpty(wsUrl)) { + Log.e(TAG, "WebSocketConfig or URL is null"); + return; + } + + try { + Request request = new Request.Builder() + .url(wsUrl) + .build(); + + webSocket = client.newWebSocket(request, new InnerWebSocketListener()); + Log.d(TAG, "Connecting to WebSocket: " + wsUrl); + + } catch (Exception e) { + Log.e(TAG, "Connect failed: " + e.getMessage()); + notifyError("连接失败: " + e.getMessage()); + } + } + + /** + * 发送文本消息 + */ + public boolean sendMessage(String message) { + if (webSocket != null && isConnected) { + boolean sent = webSocket.send(message); + if (sent) { + Log.d(TAG, "Message sent: " + message); + } else { + // 发送失败,加入队列 + messageQueue.offer(message); + Log.w(TAG, "消息发送失败,已加入队列: " + message); + } + return sent; + } else { + // 未连接,加入队列等待重发 + messageQueue.offer(message); + Log.e(TAG, "WebSocket not connected, cannot send message"); + notifyError("WebSocket未连接,无法发送消息"); + return false; + } + } + + /** + * 发送二进制消息 + */ + public boolean sendMessage(ByteString bytes) { + if (webSocket != null && isConnected) { + return webSocket.send(bytes); + } else { + Log.e(TAG, "WebSocket not connected, cannot send binary message"); + return false; + } + } + + /** + * 关闭 WebSocket 连接 + */ + public void disconnect() { + Log.d(TAG, "Disconnecting WebSocket"); + + shouldReconnect = false; + + // 停止重连和心跳 + stopReconnect(); + stopHeartbeat(); + + if (webSocket != null) { + webSocket.close(1000, "正常关闭"); + webSocket = null; + } + + isConnected = false; + Log.d(TAG, "WebSocket 连接已关闭"); + reconnectAttempts = 0; + } + + /** + * 获取连接状态 + */ + public boolean isConnected() { + return isConnected; + } + + /** + * 开始心跳检测 + */ + private void startHeartbeat() { + stopHeartbeat(); + + heartbeatRunnable = new Runnable() { + @Override + public void run() { + if (isConnected && webSocket != null) { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("msg_type", 0); + jsonObject.addProperty("content", "ping"); + // 发送心跳消息(可以是空消息或特定协议) + boolean sent = webSocket.send(jsonObject.toString()); + Log.d(TAG, "发送心跳包"); + if (sent) { + // 继续下一次心跳 + mainHandler.postDelayed(this, WebSocketConfig.heartbeatInterval); + } else { + // 发送失败,尝试重连 + handleDisconnect("Heartbeat failed"); + } + } + } + }; + mainHandler.postDelayed(heartbeatRunnable, WebSocketConfig.heartbeatInterval); + Log.d(TAG, "Heartbeat started"); + } + + /** + * 处理断开连接 + */ + private void handleDisconnect(String reason) { + Log.d(TAG, "Handle disconnect: " + reason); + + isConnected = false; + stopHeartbeat(); + + notifyDisconnected(reason); + + // 自动重连逻辑 + if (WebSocketConfig.autoReconnect && reconnectAttempts < WebSocketConfig.maxReconnectAttempts) { + scheduleReconnect(); + } + } + + /** + * 安排重连 + */ + private void scheduleReconnect() { + if (!shouldReconnect) return; + + reconnectAttempts++; + Log.d(TAG, "Scheduling reconnect attempt " + reconnectAttempts + " after " + WebSocketConfig.reconnectInterval + "ms"); + + reconnectRunnable = new Runnable() { + @Override + public void run() { + Log.d(TAG, "尝试重连 WebSocket"); + connect(); + } + }; + mainHandler.postDelayed(reconnectRunnable, WebSocketConfig.heartbeatInterval); + } + + /** + * 停止重连 + */ + private void stopReconnect() { + if (heartbeatRunnable != null) { + mainHandler.removeCallbacks(heartbeatRunnable); + heartbeatRunnable = null; + } + reconnectAttempts = 0; + } + + /** + * 停止心跳检测 + */ + private void stopHeartbeat() { + if (heartbeatRunnable != null) { + mainHandler.removeCallbacks(heartbeatRunnable); + heartbeatRunnable = null; + } + reconnectAttempts = 0; + } + + /** + * 内部WebSocket监听器 + */ + private class InnerWebSocketListener extends WebSocketListener { + @Override + public void onOpen(@NonNull WebSocket webSocket, @NonNull Response response) { + Log.d(TAG, "WebSocket connected successfully"); + + isConnected = true; + reconnectAttempts = 0; // 重置重连计数器 + + // 连接成功,发送队列中的积压消息 + processMessageQueue(); + + // 在主线程通知连接成功 + mainHandler.post(() -> { + if (callback != null) { + callback.onConnected(); + } + }); + + // 开始心跳检测 + startHeartbeat(); + } + + @Override + public void onMessage(@NonNull WebSocket webSocket, @NonNull String text) { + Log.d(TAG, "Received text message: " + text); + + // 在主线程通知消息接收 + mainHandler.post(() -> { + if (callback != null) { + callback.onMessage(text); + } + }); + } + + @Override + public void onMessage(@NonNull WebSocket webSocket, @NonNull ByteString bytes) { + Log.d(TAG, "Received binary message, size: " + bytes.size()); + + // 在主线程通知消息接收 + mainHandler.post(() -> { + if (callback != null) { + callback.onMessage(bytes); + } + }); + } + + @Override + public void onClosing(@NonNull WebSocket webSocket, int code, @NonNull String reason) { + Log.d(TAG, "WebSocket closing: " + code + " - " + reason); + webSocket.close(1000, null); + } + + @Override + public void onClosed(@NonNull WebSocket webSocket, int code, @NonNull String reason) { + Log.d(TAG, "WebSocket closed: " + code + " - " + reason); + handleDisconnect("Connection closed: " + reason); + } + + @Override + public void onFailure(@NonNull WebSocket webSocket, @NonNull Throwable t, Response response) { + Log.e(TAG, "WebSocket failure: " + t.getMessage()); + handleDisconnect("Connection failure: " + t.getMessage()); + } + } + + /** + * 处理消息队列(发送所有积压消息) + */ + private void processMessageQueue() { + new Thread(() -> { + synchronized (connectionLock) { + while (!messageQueue.isEmpty()) { + String message = messageQueue.poll(); + if (message != null && isConnected && webSocket != null) { + boolean sent = webSocket.send(message); + if (sent) { + Log.d(TAG, "队列消息发送成功: " + message); + } else { + // 发送失败,重新放回队列头部 + messageQueue.offer(message); + Log.e(TAG, "队列消息发送失败,已重新排队: " + message); + break; // 暂停发送,避免连续失败 + } + + // 添加短暂延迟,避免洪水攻击服务器 + try { + Thread.sleep(50); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + } + } + }).start(); + } + + /** + * 获取队列大小(用于监控) + */ + public int getQueueSize() { + return messageQueue.size(); + } + + /** + * 清空消息队列 + */ + public void clearQueue() { + messageQueue.clear(); + Log.d(TAG, "消息队列已清空"); + } + + /** + * 通知错误(主线程) + */ + private void notifyError(String error) { + mainHandler.post(() -> { + if (callback != null) { + callback.onError(error); + } + }); + } + + /** + * 通知断开连接(主线程) + */ + private void notifyDisconnected(String reason) { + mainHandler.post(() -> { + if (callback != null) { + callback.onDisconnected(reason); + } + }); + } + + /** + * 清理资源 + */ + public void release() { + disconnect(); + if (client != null) { + client.dispatcher().executorService().shutdown(); + } + instance = null; + Log.d(TAG, "WebSocketManager released"); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ttstd/remoteservice/utils/SystemUtils.java b/app/src/main/java/com/ttstd/remoteservice/utils/SystemUtils.java index d81529c..259c264 100644 --- a/app/src/main/java/com/ttstd/remoteservice/utils/SystemUtils.java +++ b/app/src/main/java/com/ttstd/remoteservice/utils/SystemUtils.java @@ -1,12 +1,41 @@ package com.ttstd.remoteservice.utils; +import android.annotation.SuppressLint; import android.app.ActivityManager; import android.content.Context; +import android.os.Build; +import android.util.Log; +import java.lang.reflect.Method; import java.util.List; public class SystemUtils { + /** + * 获取设备序列号 + * + * @return + */ + @SuppressLint("MissingPermission") + public static String getSerial() { + String serial = "unknow"; + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {//9.0+ + serial = Build.getSerial(); + } else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {//8.0+ + serial = Build.SERIAL; + } else {//8.0- + Class c = Class.forName("android.os.SystemProperties"); + Method get = c.getMethod("get", String.class); + serial = (String) get.invoke(c, "ro.serialno"); + } + } catch (Exception e) { + e.printStackTrace(); + Log.e("e", "读取设备序列号异常:" + e.toString()); + } + return serial; + } + public static boolean isMainProcessName(Context cxt, int pid) { String packageName = cxt.getPackageName(); ActivityManager am = (ActivityManager) cxt.getSystemService(Context.ACTIVITY_SERVICE); diff --git a/app/src/main/res/layout/activity_webrtc_screen_capture.xml b/app/src/main/res/layout/activity_webrtc_screen_capture.xml index d371f2d..3c93bc1 100644 --- a/app/src/main/res/layout/activity_webrtc_screen_capture.xml +++ b/app/src/main/res/layout/activity_webrtc_screen_capture.xml @@ -1,26 +1,57 @@ - + -