diff --git a/app/build.gradle b/app/build.gradle
index 2c442e7..d426419 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -3,13 +3,13 @@ apply plugin: 'com.android.application'
android {
namespace "com.ttstd.remoteclient"
- compileSdkVersion 33
+ compileSdkVersion 36
// buildToolsVersion "36.0.0"
defaultConfig {
applicationId "com.ttstd.remoteclient"
minSdkVersion 24
- targetSdkVersion 33
+ targetSdkVersion 36
versionCode 1
versionName "1.0"
@@ -50,6 +50,7 @@ android {
}
dependencies {
+ implementation 'androidx.activity:activity-ktx:1.13.0'
implementation fileTree(dir: 'libs', include: ['*.jar'])
// 添加 Kotlin 标准库
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 53a9889..6079280 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,16 +1,14 @@
-
-
-
-
-
+
+
-
-
-
+
+
+
+
+
+ android:exported="true"
+ android:theme="@style/AppThemeFitsSystem">
-
\ No newline at end of file
diff --git a/app/src/main/java/com/ttstd/remoteclient/activity/display/DisplayActivity.java b/app/src/main/java/com/ttstd/remoteclient/activity/display/DisplayActivity.java
new file mode 100644
index 0000000..8d928d4
--- /dev/null
+++ b/app/src/main/java/com/ttstd/remoteclient/activity/display/DisplayActivity.java
@@ -0,0 +1,26 @@
+package com.ttstd.remoteclient.activity.display;
+
+import android.os.Bundle;
+
+import androidx.activity.EdgeToEdge;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.graphics.Insets;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsCompat;
+
+import com.ttstd.remoteclient.R;
+
+public class DisplayActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ EdgeToEdge.enable(this);
+ setContentView(R.layout.activity_display);
+ ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
+ Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
+ v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
+ return insets;
+ });
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/ttstd/remoteclient/activity/main/MainActivity.java b/app/src/main/java/com/ttstd/remoteclient/activity/main/MainActivity.java
index a564554..9e969e8 100644
--- a/app/src/main/java/com/ttstd/remoteclient/activity/main/MainActivity.java
+++ b/app/src/main/java/com/ttstd/remoteclient/activity/main/MainActivity.java
@@ -15,13 +15,13 @@ import androidx.core.content.ContextCompat;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
-import com.google.gson.reflect.TypeToken;
import com.ttstd.remoteclient.R;
import com.ttstd.remoteclient.base.mvvm.BaseMvvmActivity;
+import com.ttstd.remoteclient.bean.SignalingMessage;
+import com.ttstd.remoteclient.bean.WebRTCMessage;
import com.ttstd.remoteclient.databinding.ActivityMainBinding;
-import com.ttstd.remoteclient.gson.GsonUtils;
-import com.ttstd.remoteclient.manager.SignalingWebSocketClient;
-import com.ttstd.remoteclient.utils.SystemUtils;
+import com.ttstd.remoteclient.manager.WebSocketCallback;
+import com.ttstd.remoteclient.manager.WebSocketManager;
import org.webrtc.DataChannel;
import org.webrtc.DefaultVideoDecoderFactory;
@@ -37,28 +37,36 @@ import org.webrtc.SdpObserver;
import org.webrtc.SessionDescription;
import org.webrtc.VideoTrack;
-import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
-public class MainActivity extends BaseMvvmActivity implements SignalingWebSocketClient.SignalingListener {
+import okio.ByteString;
+
+public class MainActivity extends BaseMvvmActivity implements WebSocketCallback {
private static final String TAG = "MainActivity";
+ private WebSocketManager mWebSocketManager;
+
private static final int NETWORK_REQUEST_CODE = 1001;
- // ICE服务器配置(必须,用于NAT穿透,公共免费ICE服务器)
+ // ICE服务器配置(用于NAT穿透)
private static final List ICE_SERVERS = new ArrayList() {{
add(PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer());
+ add(PeerConnection.IceServer.builder("stun:stun1.l.google.com:19302").createIceServer());
+ add(PeerConnection.IceServer.builder("stun:stun2.l.google.com:19302").createIceServer());
add(PeerConnection.IceServer.builder("stun:175.178.213.60:3478").createIceServer());
}};
- // WebRTC 核心对象(核心引擎)
private PeerConnectionFactory peerConnectionFactory;
private PeerConnection peerConnection;
private EglBase rootEglBase;
- private SignalingWebSocketClient signalingClient;
private String mRemoteUserId;
+ private boolean mIsWebRTCReady = false;
+
+ private final List mPendingIceCandidates = new ArrayList<>();
+
+ private final Gson mGson = new Gson();
@Override
public boolean setNightMode() {
@@ -80,32 +88,44 @@ public class MainActivity extends BaseMvvmActivity 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
initWebRTC();
} else {
- Toast.makeText(this, "权限被拒绝,无法使用屏幕共享功能", Toast.LENGTH_SHORT).show();
+ Toast.makeText(this, "权限被拒绝,无法使用相关功能", Toast.LENGTH_SHORT).show();
}
}
}
- // 权限申请逻辑
private void requestPermissions() {
String[] permissions = {Manifest.permission.INTERNET, Manifest.permission.ACCESS_NETWORK_STATE};
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- if (ContextCompat.checkSelfPermission(this, permissions[0]) != PackageManager.PERMISSION_GRANTED) {
+ boolean allGranted = true;
+ for (String p : permissions) {
+ if (ContextCompat.checkSelfPermission(this, p) != PackageManager.PERMISSION_GRANTED) {
+ allGranted = false;
+ break;
+ }
+ }
+ if (!allGranted) {
ActivityCompat.requestPermissions(this, permissions, NETWORK_REQUEST_CODE);
} else {
initWebRTC();
@@ -115,388 +135,466 @@ public class MainActivity extends BaseMvvmActivity mViewDataBinding.tvStatus.setText("收集到ICE候选地址,发送给远端..."));
- // TODO: 这里将 iceCandidate 转换成JSON,通过你的信令服务器发送给远端设备
- sendIceCandidateToRemote(iceCandidate);
- }
-
- // 2. ICE连接状态变化回调
- @Override
- public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
- Log.e(TAG, "onIceConnectionChange: ");
- runOnUiThread(() -> {
- mViewDataBinding.tvStatus.setText("连接状态:" + iceConnectionState.name());
- if (iceConnectionState == PeerConnection.IceConnectionState.CONNECTED) {
- mViewDataBinding.tvStatus.setText("✅ 已成功连接,正在接收远程屏幕...");
- } else if (iceConnectionState == PeerConnection.IceConnectionState.DISCONNECTED) {
- mViewDataBinding.tvStatus.setText("❌ 连接断开");
- }
- });
- }
-
- // 3. 收到远端媒体流回调【核心】:收到远程屏幕流,渲染到控件上
- @Override
- public void onAddStream(MediaStream mediaStream) {
- Log.e(TAG, "onAddStream: ");
- runOnUiThread(() -> mViewDataBinding.tvStatus.setText("✅ 收到远程屏幕流,开始渲染..."));
- // 获取视频轨道,添加到渲染控件
- VideoTrack videoTrack = mediaStream.videoTracks.get(0);
- videoTrack.addSink(mViewDataBinding.remoteVideoView);
- }
-
- // 其他生命周期回调,默认实现即可
- @Override
- public void onSignalingChange(PeerConnection.SignalingState signalingState) {
- Log.e(TAG, "onSignalingChange: ");
- }
-
- @Override
- public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
- Log.e(TAG, "onIceGatheringChange: ");
- }
-
- @Override
- public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {
- Log.e(TAG, "onIceCandidatesRemoved: ");
- }
-
- @Override
- public void onConnectionChange(PeerConnection.PeerConnectionState newState) {
- Log.e(TAG, "onConnectionChange: ");
- }
-
- @Override
- public void onIceConnectionReceivingChange(boolean b) {
- Log.e(TAG, "onIceConnectionReceivingChange: ");
- }
-
- @Override
- public void onRemoveStream(MediaStream mediaStream) {
- Log.e(TAG, "onRemoveStream: ");
- }
-
- @Override
- public void onDataChannel(DataChannel dataChannel) {
- Log.e(TAG, "onDataChannel: ");
- }
-
- @Override
- public void onRenegotiationNeeded() {
- Log.e(TAG, "onRenegotiationNeeded: ");
- }
-
- @Override
- public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {
- Log.e(TAG, "onAddTrack: ");
- }
- });
- }
-
- // ======================== 核心信令交互方法(对外提供调用) ========================
-
- /**
- * 1. 收到远端发送的 SDP Offer 数据(信令协商第一步)
- *
- * @param sdpJson 远端发送的Offer-SDP字符串(JSON格式)
- */
- public void onReceiveRemoteOffer(String sdpJson) {
- SessionDescription offerSdp = new SessionDescription(SessionDescription.Type.OFFER, sdpJson);
- peerConnection.setRemoteDescription(new SdpObserver() {
- @Override
- public void onCreateSuccess(SessionDescription sessionDescription) {
- Log.e("setRemoteDescription", "onCreateSuccess: ");
- }
-
- @Override
- public void onSetSuccess() {
- Log.e("setRemoteDescription", "onCreateSuccess: ");
- // 设置远端Offer成功,生成本地Answer并发送给远端
- peerConnection.createAnswer(new SdpObserver() {
- @Override
- public void onCreateSuccess(SessionDescription answerSdp) {
- peerConnection.setLocalDescription(new SdpObserver() {
- @Override
- public void onCreateSuccess(SessionDescription sessionDescription) {
- Log.e("setLocalDescription", "onCreateSuccess: ");
- }
-
- @Override
- public void onSetSuccess() {
- Log.e("setLocalDescription", "onSetSuccess: ");
- // TODO: 将本地Answer-SDP发送给远端
- sendSdpAnswerToRemote(answerSdp);
- }
-
- @Override
- public void onCreateFailure(String s) {
- Log.e("setLocalDescription", "onCreateFailure: " + s);
- }
-
- @Override
- public void onSetFailure(String s) {
- Log.e("setLocalDescription", "onSetFailure: " + s);
- }
- }, answerSdp);
- }
-
- @Override
- public void onCreateFailure(String s) {
- Log.e("createAnswer", "onCreateFailure: " + s);
- }
-
- @Override
- public void onSetSuccess() {
- Log.e("createAnswer", "onSetSuccess: ");
- }
-
- @Override
- public void onSetFailure(String s) {
- Log.e("createAnswer", "onSetFailure: " + s);
- }
- }, new MediaConstraints());
- }
-
- @Override
- public void onCreateFailure(String s) {
- Log.e("setRemoteDescription", "onCreateFailure: " + s);
- }
-
- @Override
- public void onSetFailure(String s) {
- Log.e("setRemoteDescription", "onSetFailure: " + s);
- }
- }, offerSdp);
- }
-
- /**
- * 2. 收到远端发送的 ICE候选地址
- *
- * @param iceCandidateJson 远端的ICE地址字符串(JSON格式)
- * @param sdpMid ICE的mid标识
- * @param sdpMLineIndex ICE的索引
- */
- public void onReceiveRemoteIceCandidate(String iceCandidateJson, String sdpMid, int sdpMLineIndex) {
- Log.e(TAG, "onReceiveRemoteIceCandidate: ");
-
- IceCandidate iceCandidate = new IceCandidate(sdpMid, sdpMLineIndex, iceCandidateJson);
- peerConnection.addIceCandidate(iceCandidate);
- }
-
- // ======================== 信令发送的桥接方法(需要你对接自己的信令服务器) ========================
-
- /**
- * 发送本地ICE候选地址到远端
- * TODO: 这里替换成你的信令逻辑(WebSocket/Http长连接/MQTT等)
- */
- private void sendIceCandidateToRemote(IceCandidate iceCandidate) {
- Log.e(TAG, "sendIceCandidateToRemote: ");
-
- String mid = iceCandidate.sdpMid;
- int index = iceCandidate.sdpMLineIndex;
- String sdp = iceCandidate.sdp;
- // 调用你的信令发送接口,把iceJson、mid、index发给远端
-
- JsonObject jsonObject = new JsonObject();
- jsonObject.addProperty("msg_type", 4);
-
- // 发送信令消息
- JsonObject iceCandidateInfo = new JsonObject();
- iceCandidateInfo.addProperty("sdpMid", mid);
- iceCandidateInfo.addProperty("sdpMLineIndex", index);
- iceCandidateInfo.addProperty("sdp", sdp);
-
- iceCandidateInfo.addProperty("fromUser", SystemUtils.getSerial());
- iceCandidateInfo.addProperty("toUser", mRemoteUserId);
- iceCandidateInfo.addProperty("timestamp", System.currentTimeMillis());
-
- jsonObject.add("content", iceCandidateInfo);
- }
-
- /**
- * 发送本地Answer-SDP到远端
- * TODO: 这里替换成你的信令逻辑
- */
- private void sendSdpAnswerToRemote(SessionDescription description) {
- Log.e(TAG, "sendIceCandidateToRemote: ");
-
- // 调用你的信令发送接口,把sdpAnswer发给远端
- JsonObject jsonObject = new JsonObject();
- jsonObject.addProperty("msg_type", 3);
-
- // 发送信令消息
- JsonObject signal = new JsonObject();
- signal.addProperty("type", description.type.canonicalForm()); // 示例:offer信令
- signal.addProperty("sdp", description.description);
-
- signal.addProperty("fromUser", SystemUtils.getSerial());
- signal.addProperty("toUser", mRemoteUserId);
- signal.addProperty("timestamp", System.currentTimeMillis());
-
- jsonObject.add("content", signal);
-
- signalingClient.sendSignal(jsonObject.toString());
- }
-
- // ======================== 生命周期管理 + 资源释放(必须,防止内存泄漏) ========================
- @Override
- protected void onDestroy() {
- super.onDestroy();
- // 释放渲染控件
- if (mViewDataBinding.remoteVideoView != null) {
- mViewDataBinding.remoteVideoView.release();
+ Log.e(TAG, "initWebRTC");
+ if (rootEglBase == null) {
+ rootEglBase = EglBase.create();
}
- // 释放P2P连接
+
+ mViewDataBinding.remoteVideoView.init(rootEglBase.getEglBaseContext(), null);
+ mViewDataBinding.remoteVideoView.setMirror(false);
+ mViewDataBinding.remoteVideoView.setEnableHardwareScaler(true);
+
+ if (peerConnectionFactory == null) {
+ PeerConnectionFactory.InitializationOptions initOptions = PeerConnectionFactory.InitializationOptions
+ .builder(this)
+ .setEnableInternalTracer(true)
+ .createInitializationOptions();
+ PeerConnectionFactory.initialize(initOptions);
+
+ PeerConnectionFactory.Options factoryOptions = new PeerConnectionFactory.Options();
+ peerConnectionFactory = PeerConnectionFactory.builder()
+ .setOptions(factoryOptions)
+ .setVideoDecoderFactory(new DefaultVideoDecoderFactory(rootEglBase.getEglBaseContext()))
+ .setVideoEncoderFactory(new DefaultVideoEncoderFactory(rootEglBase.getEglBaseContext(), true, true))
+ .createPeerConnectionFactory();
+ }
+
+ mIsWebRTCReady = true;
+ mViewDataBinding.tvStatus.setText("WebRTC引擎就绪");
+ checkAndPerformConnect();
+ }
+
+ private void createPeerConnection() {
+ Log.e(TAG, "createPeerConnection");
if (peerConnection != null) {
peerConnection.close();
peerConnection.dispose();
}
- // 释放工厂
- if (peerConnectionFactory != null) {
- peerConnectionFactory.dispose();
- }
- // 释放EGL环境
- if (rootEglBase != null) {
- rootEglBase.release();
- }
- if (signalingClient != null) {
- signalingClient.disconnect();
- }
+ PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(ICE_SERVERS);
+ peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, new PeerConnection.Observer() {
+ @Override
+ public void onIceCandidate(IceCandidate iceCandidate) {
+ Log.e(TAG, "onIceCandidate");
+ sendIceCandidateToRemote(iceCandidate);
+ }
+
+ @Override
+ public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
+ Log.e(TAG, "onIceConnectionChange: " + iceConnectionState);
+ runOnUiThread(() -> {
+ updateStatus("连接状态: " + iceConnectionState.name());
+ if (iceConnectionState == PeerConnection.IceConnectionState.CONNECTED) {
+ Log.e(TAG, "onIceConnectionChange: ✅ 已连接,正在接收远程屏幕" );
+ updateStatus("✅ 已连接,正在接收远程屏幕...");
+ } else if (iceConnectionState == PeerConnection.IceConnectionState.DISCONNECTED) {
+ updateStatus("❌ 连接已断开");
+ } else if (iceConnectionState == PeerConnection.IceConnectionState.FAILED) {
+ updateStatus("❌ 连接失败");
+ }
+ });
+ }
+
+ @Override
+ public void onAddStream(MediaStream mediaStream) {
+ Log.e(TAG, "onAddStream");
+ runOnUiThread(() -> {
+ Log.e(TAG, "onAddStream: ✅ 收到远程媒体流" );
+ updateStatus("✅ 收到远程媒体流");
+ if (!mediaStream.videoTracks.isEmpty()) {
+ VideoTrack videoTrack = mediaStream.videoTracks.get(0);
+ videoTrack.addSink(mViewDataBinding.remoteVideoView);
+ }
+ });
+ }
+
+ @Override
+ public void onSignalingChange(PeerConnection.SignalingState signalingState) {
+ Log.e(TAG, "onSignalingChange: " + signalingState);
+ }
+
+ @Override
+ public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
+ Log.e(TAG, "onIceGatheringChange: " + iceGatheringState);
+ }
+
+ @Override
+ public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {
+ }
+
+ @Override
+ public void onConnectionChange(PeerConnection.PeerConnectionState newState) {
+ Log.e(TAG, "onConnectionChange: " + newState);
+ }
+
+ @Override
+ public void onIceConnectionReceivingChange(boolean b) {
+ }
+
+ @Override
+ public void onRemoveStream(MediaStream mediaStream) {
+ }
+
+ @Override
+ public void onDataChannel(DataChannel dataChannel) {
+ }
+
+ @Override
+ public void onRenegotiationNeeded() {
+ Log.e(TAG, "onRenegotiationNeeded");
+ }
+
+ @Override
+ public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {
+ }
+ });
+ }
+
+ // ======================== 信令交互 ========================
+
+ private void sendSignalingMessage(String type, SignalingMessage message) {
+ if (mWebSocketManager == null || !mWebSocketManager.isConnected()) return;
+
+ // 按照服务器要求的包装格式发送
+ JsonObject wrapper = new JsonObject();
+ wrapper.addProperty("type", type);
+ wrapper.addProperty("target", mRemoteUserId);
+ wrapper.add("data", mGson.toJsonTree(message));
+
+ mWebSocketManager.sendSignal(wrapper.toString());
+ }
+
+ private void sendInitToRemote() {
+ SignalingMessage message = new SignalingMessage("init", mWebSocketManager.getLocalUserId(), mRemoteUserId, "Ready to connect");
+ sendSignalingMessage("init", message);
+ }
+
+ private void sendAskToRemote() {
+ SignalingMessage message = new SignalingMessage("ask", mWebSocketManager.getLocalUserId(), mRemoteUserId, "ask to " + mRemoteUserId);
+ sendSignalingMessage("ask", message);
+ }
+
+ private void sendOfferToRemote() {
+ SignalingMessage message = new SignalingMessage("offer", mWebSocketManager.getLocalUserId(), mRemoteUserId, "offer to " + mRemoteUserId);
+ sendSignalingMessage("offer", message);
}
- // 实现 SignalingListener 接口方法
+ private void sendIceCandidateToRemote(IceCandidate iceCandidate) {
+ if (mWebSocketManager != null && mWebSocketManager.isConnected()) {
+ SignalingMessage message = new SignalingMessage("iceCandidate", mWebSocketManager.getLocalUserId(), mRemoteUserId, iceCandidate);
+ sendSignalingMessage("iceCandidate", message);
+ } else {
+ Log.e(TAG, "Signaling not connected, queueing ice candidate");
+ mPendingIceCandidates.add(iceCandidate);
+ }
+ }
+
+ private void sendIceCandidateToRemote() {
+ if (peerConnection == null) {
+ createPeerConnection();
+ }
+ SignalingMessage message = new SignalingMessage("iceCandidate", mWebSocketManager.getLocalUserId(), mRemoteUserId, "offer to remote");
+ sendSignalingMessage("iceCandidate", message);
+ }
+
+ private void sendSdpAnswerToRemote(SessionDescription description) {
+ SignalingMessage message = new SignalingMessage("answer", mWebSocketManager.getLocalUserId(), mRemoteUserId, description);
+ sendSignalingMessage("answer", message);
+ }
+
+ public void onReceiveRemoteOffer(String jsonString) {
+
+ Log.e(TAG, "onReceiveRemoteOffer");
+ if (peerConnection == null) {
+ createPeerConnection();
+ }
+
+ SignalingMessage message = mGson.fromJson(jsonString, SignalingMessage.class);
+ SessionDescription offerSdp = mGson.fromJson(mGson.toJson(message.getPayload()), SessionDescription.class);
+
+ peerConnection.setRemoteDescription(new SimpleSdpObserver("setRemoteDescription") {
+ @Override
+ public void onSetSuccess() {
+ super.onSetSuccess();
+ peerConnection.createAnswer(new SimpleSdpObserver("createAnswer") {
+ @Override
+ public void onCreateSuccess(SessionDescription sessionDescription) {
+ Log.e(TAG, "onReceiveRemoteOffer onCreateSuccess: " );
+ super.onCreateSuccess(sessionDescription);
+ peerConnection.setLocalDescription(new SimpleSdpObserver("setLocalDescription") {
+ @Override
+ public void onSetSuccess() {
+ super.onSetSuccess();
+ sendSdpAnswerToRemote(sessionDescription);
+ }
+ }, sessionDescription);
+ }
+ }, new MediaConstraints());
+ }
+ }, offerSdp);
+ }
+
+ public void onReceiveRemoteAnswer(String sdp) {
+ Log.e(TAG, "onReceiveRemoteAnswer");
+ if (peerConnection != null) {
+ SessionDescription answerSdp = new SessionDescription(SessionDescription.Type.ANSWER, sdp);
+ peerConnection.setRemoteDescription(new SimpleSdpObserver("setRemoteAnswer"), answerSdp);
+ }
+ }
+
+ public void onReceiveRemoteIceCandidate(String sdp, String sdpMid, int sdpMLineIndex) {
+ Log.e(TAG, "onReceiveRemoteIceCandidate");
+ if (peerConnection != null) {
+ IceCandidate iceCandidate = new IceCandidate(sdpMid, sdpMLineIndex, sdp);
+ peerConnection.addIceCandidate(iceCandidate);
+ }
+ }
+
+ // ======================== SignalingListener ========================
+
@Override
public void onConnected(String sessionId) {
- Log.e(TAG, "onConnected: " + sessionId);
-
- // 调用你的信令发送接口,把sdpAnswer发给远端
- JsonObject jsonObject = new JsonObject();
- jsonObject.addProperty("msg_type", 3);
-
- // 发送信令消息
- JsonObject signal = new JsonObject();
- signal.addProperty("type", "offer"); // 示例:offer信令
- signal.addProperty("fromUser", SystemUtils.getSerial());
- signal.addProperty("toUser", mRemoteUserId);
- signal.addProperty("timestamp", System.currentTimeMillis());
-
- jsonObject.add("content", signal);
-
- signalingClient.sendSignal(jsonObject.toString());
-
+ Log.e(TAG, "Signaling connected: " + sessionId);
runOnUiThread(() -> {
- updateStatus("已连接,会话ID: " + sessionId);
- addMessage("系统: 连接到信令服务器");
+ updateWsStatus("已连接到信令服务器");
+ mViewDataBinding.btConnect.setEnabled(false);
+
+ // 发送暂存的 ICE Candidate
+ if (!mPendingIceCandidates.isEmpty()) {
+ Log.e(TAG, "Sending " + mPendingIceCandidates.size() + " pending ice candidates");
+ for (IceCandidate candidate : mPendingIceCandidates) {
+ sendIceCandidateToRemote(candidate);
+ }
+ mPendingIceCandidates.clear();
+ }
+
+ checkAndPerformConnect();
});
}
- @Override
- public void onMessageReceived(String msgType, String data) {
- Log.e(TAG, "onMessageReceived: type = " + msgType);
- Log.e(TAG, "onMessageReceived: data = " + data);
- JsonObject jsonObject = GsonUtils.getJsonObject(data);
- int msg_type = jsonObject.get("msg_type").getAsInt();
- if (msg_type == 11) {
- String sdpJson = jsonObject.get("sdp").getAsString();
- onReceiveRemoteOffer(sdpJson);
- } else if (msg_type == 22) {
- String iceJsonString = jsonObject.get("ice").getAsString();
- JsonObject iceJson = GsonUtils.getJsonObject(iceJsonString);
- String sdpMid = iceJson.get("sdpMid").getAsString();
- int sdpMLineIndex = iceJson.get("sdpMLineIndex").getAsInt();
- String sdp = iceJson.get("sdp").getAsString();
-
- onReceiveRemoteIceCandidate(sdp, sdpMid, sdpMLineIndex);
+ private synchronized void checkAndPerformConnect() {
+ if (mIsWebRTCReady && mWebSocketManager != null && mWebSocketManager.isConnected() && !TextUtils.isEmpty(mRemoteUserId)) {
+ Log.e(TAG, "Both WebRTC and Signaling are ready, sending init signal...");
+ // 发送初始化消息告知对方我已上线并想连接
+ sendInitToRemote();
+ // sendIceCandidateToRemote(); // 现在改为手动点击 bt_stream 发送
+ runOnUiThread(() -> updateStatus("已向设备 " + mRemoteUserId + " 发送初始化消息..."));
}
- runOnUiThread(() -> {
- addMessage("收到[" + msgType + "]: " + data);
- });
}
@Override
public void onDisconnected(String reason) {
- Log.e(TAG, "onDisconnected: ");
runOnUiThread(() -> {
- updateStatus("已断开: " + reason);
- addMessage("系统: 连接断开 - " + reason);
+ updateWsStatus("信令已断开: " + reason);
+ mViewDataBinding.btConnect.setEnabled(true);
});
}
@Override
- public void onError(String error) {
- Log.e(TAG, "onError: " + error);
+ public void onMessage(String message) {
+ Log.e(TAG, "onMessageReceived: " + message);
+ try {
+ WebRTCMessage webRTCMessage = mGson.fromJson(message, WebRTCMessage.class);
+ String type = webRTCMessage.getType();
+ Object data = webRTCMessage.getData();
+
+ // 尝试解析为 SignalingMessage
+ SignalingMessage sm = null;
+ if (data != null) {
+ if (data instanceof String) {
+ try {
+ sm = mGson.fromJson((String) data, SignalingMessage.class);
+ } catch (Exception ignored) {}
+ } else {
+ try {
+ sm = mGson.fromJson(mGson.toJson(data), SignalingMessage.class);
+ } catch (Exception ignored) {}
+ }
+ }
+
+ switch (type) {
+ case "offer":
+ onReceiveRemoteOffer(sm != null ? mGson.toJson(sm) : (data instanceof String ? (String) data : mGson.toJson(data)));
+ break;
+ case "answer":
+ if (sm != null && sm.getPayload() != null) {
+ if (sm.getPayload() instanceof String) {
+ onReceiveRemoteAnswer((String) sm.getPayload());
+ } else {
+ JsonObject payloadJson = mGson.toJsonTree(sm.getPayload()).getAsJsonObject();
+ if (payloadJson.has("description")) {
+ onReceiveRemoteAnswer(payloadJson.get("description").getAsString());
+ } else {
+ onReceiveRemoteAnswer(mGson.toJson(sm.getPayload()));
+ }
+ }
+ } else if (data instanceof String) {
+ onReceiveRemoteAnswer((String) data);
+ }
+ break;
+ case "reject":
+ Object rejectReason = (sm != null && sm.getPayload() != null) ? sm.getPayload() : data;
+ handleRejectMessage(rejectReason instanceof String ? (String) rejectReason : mGson.toJson(rejectReason));
+ break;
+ case "accept":
+ Object acceptReason = (sm != null && sm.getPayload() != null) ? sm.getPayload() : data;
+ handleAcceptMessage(acceptReason instanceof String ? (String) acceptReason : mGson.toJson(acceptReason));
+ break;
+ case "sdp":
+ onReceiveRemoteAnswer((String) data);
+ break;
+ case "iceCandidate":
+ Object iceData = (sm != null && sm.getPayload() != null) ? sm.getPayload() : data;
+ try {
+ JsonObject iceJson = mGson.toJsonTree(iceData).getAsJsonObject();
+ String sdp = iceJson.get("sdp").getAsString();
+ String sdpMid = iceJson.get("sdpMid").getAsString();
+ int sdpMLineIndex = iceJson.get("sdpMLineIndex").getAsInt();
+ onReceiveRemoteIceCandidate(sdp, sdpMid, sdpMLineIndex);
+ } catch (Exception e) {
+ Log.e(TAG, "Error parsing iceCandidate: " + e.getMessage());
+ }
+ break;
+ case "init":
+ // 收到初始化请求,主动发起Offer (如果是这种逻辑)
+ // 在本客户端通常是被控端发Offer,如果本端想看远程,可能需要等对方发Offer
+ break;
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error parsing signaling message", e);
+ }
+ }
+
+ private void handleAcceptMessage(String reason) {
+ Log.e(TAG, "handleAcceptMessage: reason = " + reason);
runOnUiThread(() -> {
- Log.e(TAG, "WebSocket错误: " + error);
- addMessage("错误: " + error);
+ updateWsStatus("对方拒绝了连接: " + reason);
+ Toast.makeText(this, "正在连接", Toast.LENGTH_SHORT).show();
+ });
+
+
+ }
+
+ private void handleRejectMessage(String reason) {
+ Log.e(TAG, "handleRejectMessage: reason = " + reason);
+ runOnUiThread(() -> {
+ updateWsStatus("对方拒绝了连接: " + reason);
+ mViewDataBinding.btConnect.setEnabled(true);
+ Toast.makeText(this, "对方拒绝了连接," + reason, Toast.LENGTH_SHORT).show();
+ });
+ }
+
+ @Override
+ public void onMessage(ByteString bytes) {
+
+ }
+
+ @Override
+ public void onError(String error) {
+ runOnUiThread(() -> {
+ updateWsStatus("信令错误: " + error);
+ mViewDataBinding.btConnect.setEnabled(true);
+ Toast.makeText(this, "信令错误: " + error, Toast.LENGTH_SHORT).show();
});
}
private void updateStatus(String status) {
- mViewDataBinding.tvStatus.setText("状态: " + status);
+ mViewDataBinding.tvStatus.setText(status);
}
- private void addMessage(String message) {
- String current = mViewDataBinding.tvStatus.getText().toString();
- mViewDataBinding.tvStatus.setText(current + "\n" + message);
+ private void updateWsStatus(String status) {
+ mViewDataBinding.tvWsStatus.setText(status);
}
+ // ======================== 生命周期 ========================
- public class BtnClick {
- public void connectWebRtc(View view) {
- Editable editable = mViewDataBinding.etNumber.getText();
- if (TextUtils.isEmpty(editable)) {
- Log.e(TAG, "connectWebRtc: userId is empty");
- mViewDataBinding.tvStatus.setText("用户id为空");
- return;
- }
- mRemoteUserId = editable.toString();
- // 创建信令客户端
- signalingClient = new SignalingWebSocketClient(MainActivity.this);
- signalingClient.connect();
- signalingClient.setUserId(mRemoteUserId);
- updateStatus("正在连接...");
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (mViewDataBinding.remoteVideoView != null) {
+ mViewDataBinding.remoteVideoView.release();
+ }
+ if (peerConnection != null) {
+ peerConnection.close();
+ peerConnection.dispose();
+ }
+ if (peerConnectionFactory != null) {
+ peerConnectionFactory.dispose();
+ }
+ if (rootEglBase != null) {
+ rootEglBase.release();
+ }
+ if (mWebSocketManager != null) {
+ mWebSocketManager.removeCallback(this);
}
}
+ public class BtnClick {
+ public void connectWebSocket(View view) {
+ if (mWebSocketManager != null) {
+ if (mWebSocketManager.isConnected()) {
+ updateWsStatus("已连接到信令服务器");
+ } else {
+ updateWsStatus("正在连接信令服务器...");
+ mWebSocketManager.connect();
+ }
+ } else {
+ mWebSocketManager = WebSocketManager.getInstance();
+ updateWsStatus("正在连接信令服务器...");
+ mWebSocketManager.connect();
+ }
+ }
+ public void startStream(View view) {
+ Editable editable = mViewDataBinding.etNumber.getText();
+ if (TextUtils.isEmpty(editable)) {
+ Toast.makeText(MainActivity.this, "请输入设备ID", Toast.LENGTH_SHORT).show();
+ return;
+ }
+ mRemoteUserId = editable.toString();
+ if (mWebSocketManager == null || !mWebSocketManager.isConnected()) {
+ Toast.makeText(MainActivity.this, "请先连接WebSocket", Toast.LENGTH_SHORT).show();
+ return;
+ }
+ if (TextUtils.isEmpty(mRemoteUserId)) {
+ Toast.makeText(MainActivity.this, "未指定远程设备ID", Toast.LENGTH_SHORT).show();
+ return;
+ }
+ Log.e(TAG, "Sending connection request to " + mRemoteUserId);
+ sendAskToRemote();
+ updateStatus("已发送连接请求给 " + mRemoteUserId);
+ }
+ }
+
+ // SdpObserver 简易适配器
+ private static class SimpleSdpObserver implements SdpObserver {
+ private final String tag;
+
+ SimpleSdpObserver(String tag) {
+ this.tag = tag;
+ }
+
+ @Override
+ public void onCreateSuccess(SessionDescription sessionDescription) {
+ Log.e(TAG, tag + " onCreateSuccess");
+ }
+
+ @Override
+ public void onSetSuccess() {
+ Log.e(TAG, tag + " onSetSuccess");
+ }
+
+ @Override
+ public void onCreateFailure(String s) {
+ Log.e(TAG, tag + " onCreateFailure: " + s);
+ }
+
+ @Override
+ public void onSetFailure(String s) {
+ Log.e(TAG, tag + " onSetFailure: " + s);
+ }
+ }
}
diff --git a/app/src/main/java/com/ttstd/remoteclient/base/BaseApplication.java b/app/src/main/java/com/ttstd/remoteclient/base/BaseApplication.java
index cadbf7d..2e02bd5 100644
--- a/app/src/main/java/com/ttstd/remoteclient/base/BaseApplication.java
+++ b/app/src/main/java/com/ttstd/remoteclient/base/BaseApplication.java
@@ -12,6 +12,7 @@ import com.alibaba.android.arouter.launcher.ARouter;
import com.tencent.bugly.crashreport.CrashReport;
import com.tencent.mmkv.MMKV;
import com.ttstd.remoteclient.BuildConfig;
+import com.ttstd.remoteclient.manager.WebSocketManager;
import com.ttstd.remoteclient.utils.SystemUtils;
@@ -55,6 +56,8 @@ public class BaseApplication extends Application {
}
ARouter.init(this); // 尽可能早,推荐在Application中初始化
+ WebSocketManager.init(this);
+
CrashReport.initCrashReport(getApplicationContext(), "845e3ed68c", false);
CrashReport.setDeviceId(this, Build.MODEL);
xcrash.XCrash.init(this);
diff --git a/app/src/main/java/com/ttstd/remoteclient/bean/SignalMessage.java b/app/src/main/java/com/ttstd/remoteclient/bean/SignalMessage.java
deleted file mode 100644
index d1a9695..0000000
--- a/app/src/main/java/com/ttstd/remoteclient/bean/SignalMessage.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.ttstd.remoteclient.bean;
-
-import java.io.Serializable;
-
-public class SignalMessage implements Serializable {
-
-
-
-}
diff --git a/app/src/main/java/com/ttstd/remoteclient/bean/SignalingMessage.java b/app/src/main/java/com/ttstd/remoteclient/bean/SignalingMessage.java
new file mode 100644
index 0000000..d0e916d
--- /dev/null
+++ b/app/src/main/java/com/ttstd/remoteclient/bean/SignalingMessage.java
@@ -0,0 +1,49 @@
+package com.ttstd.remoteclient.bean;
+
+import java.io.Serializable;
+
+public class SignalingMessage implements Serializable {
+ private String type; // "call", "offer", "answer", "iceCandidate", etc.
+ private String senderId; // 发送者ID
+ private String targetId; // 接收者ID
+ private Object payload; // 具体携带的数据(可以是String, SessionDescription, IceCandidate等)
+
+ public SignalingMessage(String type, String senderId, String targetId, Object payload) {
+ this.type = type;
+ this.senderId = senderId;
+ this.targetId = targetId;
+ this.payload = payload;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public String getSenderId() {
+ return senderId;
+ }
+
+ public void setSenderId(String senderId) {
+ this.senderId = senderId;
+ }
+
+ public String getTargetId() {
+ return targetId;
+ }
+
+ public void setTargetId(String targetId) {
+ this.targetId = targetId;
+ }
+
+ public Object getPayload() {
+ return payload;
+ }
+
+ public void setPayload(Object payload) {
+ this.payload = payload;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/ttstd/remoteclient/bean/WebRTCMessage.java b/app/src/main/java/com/ttstd/remoteclient/bean/WebRTCMessage.java
new file mode 100644
index 0000000..c3fbc46
--- /dev/null
+++ b/app/src/main/java/com/ttstd/remoteclient/bean/WebRTCMessage.java
@@ -0,0 +1,33 @@
+package com.ttstd.remoteclient.bean;
+
+import java.io.Serializable;
+
+public class WebRTCMessage implements Serializable {
+ private String type; // "offer", "answer", "iceCandidate", "call", "hangup", "reject", "accept"
+ private String target;
+ private Object data;
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public String getTarget() {
+ return target;
+ }
+
+ public void setTarget(String target) {
+ this.target = target;
+ }
+
+ public Object getData() {
+ return data;
+ }
+
+ public void setData(Object data) {
+ this.data = data;
+ }
+}
diff --git a/app/src/main/java/com/ttstd/remoteclient/config/WebSocketConfig.java b/app/src/main/java/com/ttstd/remoteclient/config/WebSocketConfig.java
index 06d0e7d..1070943 100644
--- a/app/src/main/java/com/ttstd/remoteclient/config/WebSocketConfig.java
+++ b/app/src/main/java/com/ttstd/remoteclient/config/WebSocketConfig.java
@@ -15,4 +15,5 @@ public class WebSocketConfig {
public static final long readTimeout = 10; // 读取超时时间(秒)
public static final long writeTimeout = 10; // 写入超时时间(秒)
+ public static final long pingInterval = 10;
}
diff --git a/app/src/main/java/com/ttstd/remoteclient/manager/SignalingWebSocketClient.java b/app/src/main/java/com/ttstd/remoteclient/manager/SignalingWebSocketClient.java
deleted file mode 100644
index 41c3612..0000000
--- a/app/src/main/java/com/ttstd/remoteclient/manager/SignalingWebSocketClient.java
+++ /dev/null
@@ -1,237 +0,0 @@
-package com.ttstd.remoteclient.manager;
-
-import android.os.Handler;
-import android.os.Looper;
-
-import com.google.gson.Gson;
-import com.google.gson.JsonObject;
-import com.ttstd.remoteclient.utils.SystemUtils;
-
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-import java.util.concurrent.TimeUnit;
-
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.Response;
-import okhttp3.WebSocket;
-import okhttp3.WebSocketListener;
-import okio.ByteString;
-
-public class SignalingWebSocketClient {
- private static final String TAG = "SignalingWebSocket";
-
- public interface SignalingListener {
- void onConnected(String sessionId);
-
- void onMessageReceived(String type, String data);
-
- void onDisconnected(String reason);
-
- void onError(String error);
- }
-
-// private static final String SERVER_URL = "ws://175.178.213.60:2310/signaling/";
- private static final String SERVER_URL = "ws://192.168.100.111:2310/signaling/";
-
- private static final int RECONNECT_DELAY_MS = 3000;
-
- private Gson gson = new Gson();
- private OkHttpClient client;
- private WebSocket webSocket;
- private Request request;
-
- private SignalingListener listener;
- private Handler mainHandler;
- private boolean mConnected = false;
- private boolean shouldReconnect = true;
-
- private String mUserId;
-
- public SignalingWebSocketClient(SignalingListener listener) {
- this.listener = listener;
- this.mainHandler = new Handler(Looper.getMainLooper());
-
- // 创建OkHttpClient,支持WebSocket
- client = new OkHttpClient.Builder()
- .pingInterval(20, TimeUnit.SECONDS) // 保持连接活跃
- .connectTimeout(10, TimeUnit.SECONDS)
- .readTimeout(10, TimeUnit.SECONDS)
- .writeTimeout(10, TimeUnit.SECONDS)
- .build();
- }
-
- public void setUserId(String userId) {
- mUserId = userId;
- }
-
- public void connect() {
- if (mConnected) {
- return;
- }
- if (webSocket != null) {
- return;
- }
-
- request = new Request.Builder()
- .url(SERVER_URL + SystemUtils.getSerial())
- .build();
-
- mConnected = false;
- shouldReconnect = true;
-
- webSocket = client.newWebSocket(request, new WebSocketListener() {
- @Override
- public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) {
- mConnected = true;
- runOnUiThread(() -> {
- if (listener != null) {
- listener.onConnected("连接成功");
- }
- });
- }
-
- @Override
- public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) {
- runOnUiThread(() -> {
- try {
- JsonObject json = gson.fromJson(text, JsonObject.class);
- String type = json.get("type").getAsString();
-
- switch (type) {
- case "connected":
- String sessionId = json.get("sessionId").getAsString();
- if (listener != null) {
- listener.onConnected(sessionId);
- }
- break;
- case "signal":
- String from = json.get("from").getAsString();
- String data = json.get("data").toString();
- if (listener != null) {
- listener.onMessageReceived("signal", "来自" + from + ": " + data);
- }
- break;
- case "ack":
- if (listener != null) {
- listener.onMessageReceived("ack", "消息发送成功");
- }
- break;
- case "peer_left":
- String leftSessionId = json.get("sessionId").getAsString();
- if (listener != null) {
- listener.onMessageReceived("peer_left", "对端离开: " + leftSessionId);
- }
- break;
- case "error":
- String errorMsg = json.get("message").getAsString();
- if (listener != null) {
- listener.onError(errorMsg);
- }
- break;
- }
- } catch (Exception e) {
- if (listener != null) {
- listener.onMessageReceived("raw", text);
- }
- }
- });
- }
-
- @Override
- public void onMessage(@NotNull WebSocket webSocket, @NotNull ByteString bytes) {
- // 处理二进制消息
- runOnUiThread(() -> {
- if (listener != null) {
- listener.onMessageReceived("binary", bytes.hex());
- }
- });
- }
-
- @Override
- public void onClosing(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
- mConnected = false;
- runOnUiThread(() -> {
- if (listener != null) {
- listener.onDisconnected("连接关闭中: " + reason);
- }
- });
- }
-
- @Override
- public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
- mConnected = false;
- webSocket = null;
-
- runOnUiThread(() -> {
- if (listener != null) {
- listener.onDisconnected("连接已关闭: " + reason);
- }
- });
-
- // 自动重连
- if (shouldReconnect) {
- mainHandler.postDelayed(() -> connect(), RECONNECT_DELAY_MS);
- }
- }
-
- @Override
- public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable t,
- @Nullable Response response) {
- mConnected = false;
- webSocket = null;
-
- runOnUiThread(() -> {
- if (listener != null) {
- listener.onError("连接失败: " + t.getMessage());
- listener.onDisconnected(t.getMessage());
- }
- });
-
- // 自动重连
- if (shouldReconnect) {
- mainHandler.postDelayed(() -> connect(), RECONNECT_DELAY_MS);
- }
- }
- });
- }
-
- public void sendSignal(String signalData) {
- if (webSocket != null) {
- webSocket.send(signalData);
- }
- }
-
- public void sendJsonSignal(JsonObject signalJson) {
- if (webSocket != null) {
- webSocket.send(gson.toJson(signalJson));
- }
- }
-
- public void disconnect() {
- mConnected = false;
- shouldReconnect = false;
-
- if (webSocket != null) {
- webSocket.close(1000, "正常断开");
- webSocket = null;
- }
-
- if (client != null) {
- client.dispatcher().executorService().shutdown();
- }
- }
-
- public boolean isConnected() {
- return webSocket != null;
- }
-
- private void runOnUiThread(Runnable runnable) {
- if (Looper.myLooper() == Looper.getMainLooper()) {
- runnable.run();
- } else {
- mainHandler.post(runnable);
- }
- }
-}
diff --git a/app/src/main/java/com/ttstd/remoteclient/manager/WebSocketCallback.java b/app/src/main/java/com/ttstd/remoteclient/manager/WebSocketCallback.java
new file mode 100644
index 0000000..524665c
--- /dev/null
+++ b/app/src/main/java/com/ttstd/remoteclient/manager/WebSocketCallback.java
@@ -0,0 +1,42 @@
+package com.ttstd.remoteclient.manager;
+
+import okio.ByteString;
+
+/**
+ * WebSocket 回调接口
+ * 所有回调都在主线程执行,方便更新 UI
+ */
+public interface WebSocketCallback {
+ /**
+ * 连接成功
+ */
+ void onConnected(String sessionId);
+
+ /**
+ * 连接断开
+ *
+ * @param reason 断开原因
+ */
+ void onDisconnected(String reason);
+
+ /**
+ * 收到文本消息
+ *
+ * @param message 文本内容
+ */
+ void onMessage(String message);
+
+ /**
+ * 收到二进制消息
+ *
+ * @param bytes 二进制数据
+ */
+ void onMessage(ByteString bytes);
+
+ /**
+ * 连接发生错误
+ *
+ * @param error 错误信息
+ */
+ void onError(String error);
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/ttstd/remoteclient/manager/WebSocketManager.java b/app/src/main/java/com/ttstd/remoteclient/manager/WebSocketManager.java
new file mode 100644
index 0000000..4a96ca6
--- /dev/null
+++ b/app/src/main/java/com/ttstd/remoteclient/manager/WebSocketManager.java
@@ -0,0 +1,213 @@
+package com.ttstd.remoteclient.manager;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.util.Log;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.ttstd.remoteclient.config.WebSocketConfig;
+import com.ttstd.remoteclient.utils.SystemUtils;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.TimeUnit;
+
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+import okhttp3.WebSocket;
+import okhttp3.WebSocketListener;
+import okio.ByteString;
+
+public class WebSocketManager {
+ private static final String TAG = "WebSocketManager";
+
+ @SuppressLint("StaticFieldLeak")
+ private static volatile WebSocketManager instance;
+
+ private final Context mContext;
+
+ //private static final String SERVER_URL = "ws://175.178.213.60:2310/signaling/";
+ private static final String SERVER_URL = "ws://192.168.100.224:2310/signaling/";
+
+ private static final int RECONNECT_DELAY_MS = 3000;
+
+ private final Gson gson = new Gson();
+ private final OkHttpClient client;
+ private final Request request;
+ private WebSocket webSocket;
+
+ private final Set mCallbackSet = new CopyOnWriteArraySet<>();
+ private boolean mConnected = false;
+ private boolean shouldReconnect = true;
+
+ private final String mLocalUserId;
+
+ private WebSocketManager(Context context) {
+ this.mContext = context.getApplicationContext();
+ this.mLocalUserId = SystemUtils.getUniqueID(mContext);
+
+ // 创建OkHttpClient,支持WebSocket
+ client = new OkHttpClient.Builder()
+ .pingInterval(WebSocketConfig.pingInterval, TimeUnit.SECONDS) // 保持连接活跃
+ .connectTimeout(WebSocketConfig.connectTimeout, TimeUnit.SECONDS)
+ .readTimeout(WebSocketConfig.readTimeout, TimeUnit.SECONDS)
+ .writeTimeout(WebSocketConfig.writeTimeout, TimeUnit.SECONDS)
+ .build();
+ request = new Request.Builder()
+ .url(SERVER_URL + mLocalUserId)
+ .build();
+
+ webSocket = client.newWebSocket(request, listener);
+ }
+
+ public static void init(Context context) {
+ if (instance == null) {
+ synchronized (WebSocketManager.class) {
+ if (instance == null) {
+ instance = new WebSocketManager(context);
+ }
+ }
+ }
+ }
+
+ public static WebSocketManager getInstance() {
+ if (instance == null) {
+ throw new IllegalStateException("WebSocketManager must be initialized first with init(Context)");
+ }
+ return instance;
+ }
+
+ public void addCallback(WebSocketCallback listener) {
+ this.mCallbackSet.add(listener);
+ if (isConnected()) {
+ listener.onConnected("连接成功");
+ }
+ }
+
+ public void removeCallback(WebSocketCallback listener) {
+ this.mCallbackSet.remove(listener);
+ }
+
+ public void connect() {
+ if (mConnected && webSocket != null) return;
+ webSocket = client.newWebSocket(request, listener);
+ }
+
+ private final WebSocketListener listener = new WebSocketListener() {
+ @Override
+ public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) {
+ Log.e(TAG, "onOpen: " + response.message());
+ mConnected = true;
+ for (WebSocketCallback callback : mCallbackSet) {
+ callback.onConnected("连接成功");
+ }
+ }
+
+ @Override
+ public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) {
+ Log.e(TAG, "onMessage: " + text);
+ for (WebSocketCallback callback : mCallbackSet) {
+ callback.onMessage(text);
+ }
+ }
+
+ @Override
+ public void onMessage(@NotNull WebSocket webSocket, @NotNull ByteString bytes) {
+ Log.d(TAG, "onMessage: Received binary message, size = " + bytes.size());
+ // 处理二进制消息
+ for (WebSocketCallback callback : mCallbackSet) {
+ callback.onMessage(bytes);
+ }
+ }
+
+ @Override
+ public void onClosing(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
+ Log.e(TAG, "onClosing: code = " + code + " reason = " + reason);
+ mConnected = false;
+ for (WebSocketCallback callback : mCallbackSet) {
+ callback.onDisconnected("连接关闭中: " + reason);
+ }
+ }
+
+ @Override
+ public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
+ Log.e(TAG, "onClosed: code = " + code + " reason = " + reason);
+ mConnected = false;
+ WebSocketManager.this.webSocket = null;
+
+ for (WebSocketCallback callback : mCallbackSet) {
+ callback.onDisconnected("连接已关闭: " + reason);
+ }
+
+ // 自动重连
+ if (shouldReconnect) {
+ // 由于删除了 Handler,且用户要求不考虑线程,这里直接重连或在子线程重连
+ // OkHttp 的回调已经在子线程,这里直接调用 connect 会立即尝试。
+ // 加上延时通常更好,但用户说不考虑线程问题,这里可以用简单的方式。
+ try {
+ Thread.sleep(RECONNECT_DELAY_MS);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ connect();
+ }
+ }
+
+ @Override
+ public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable t, @Nullable Response response) {
+ Log.e(TAG, "onFailure: Throwable = " + t.getMessage());
+ mConnected = false;
+ WebSocketManager.this.webSocket = null;
+
+ for (WebSocketCallback callback : mCallbackSet) {
+ callback.onError("检查网络");
+ callback.onDisconnected("连接失败");
+ }
+
+ // 自动重连
+ if (shouldReconnect) {
+ try {
+ Thread.sleep(RECONNECT_DELAY_MS);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ connect();
+ }
+ }
+ };
+
+ public void sendSignal(String signalData) {
+ if (webSocket != null && mConnected) {
+ webSocket.send(signalData);
+ }
+ }
+
+ public void sendJsonSignal(JsonObject signalJson) {
+ if (webSocket != null && mConnected) {
+ webSocket.send(gson.toJson(signalJson));
+ }
+ }
+
+ public void disconnect() {
+ mConnected = false;
+ shouldReconnect = false;
+
+ if (webSocket != null) {
+ webSocket.close(1000, "正常断开");
+ webSocket = null;
+ }
+ }
+
+ public boolean isConnected() {
+ return mConnected && webSocket != null;
+ }
+
+ public String getLocalUserId() {
+ return mLocalUserId;
+ }
+}
diff --git a/app/src/main/java/com/ttstd/remoteclient/utils/SystemUtils.java b/app/src/main/java/com/ttstd/remoteclient/utils/SystemUtils.java
index feca9e4..9f59b90 100644
--- a/app/src/main/java/com/ttstd/remoteclient/utils/SystemUtils.java
+++ b/app/src/main/java/com/ttstd/remoteclient/utils/SystemUtils.java
@@ -4,38 +4,22 @@ import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.content.Context;
import android.os.Build;
+import android.provider.Settings;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
import android.util.Log;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
import java.lang.reflect.Method;
import java.util.List;
+import java.util.UUID;
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);
@@ -50,4 +34,112 @@ public class SystemUtils {
}
return false;
}
+
+ private static final String CACHE_FILE_NAME = ".device_id_cache";
+ private static String sUniqueId = null;
+
+ /**
+ * 获取设备唯一标识(核心兼容方法)
+ */
+ public static String getUniqueID(Context context) {
+ if (!TextUtils.isEmpty(sUniqueId)) {
+ return sUniqueId;
+ }
+
+ Context appContext = context.getApplicationContext();
+
+ // 1. 尝试从本地缓存(内存/文件)读取,保证一致性
+ sUniqueId = readIdFromFile(appContext);
+ if (!TextUtils.isEmpty(sUniqueId)) {
+ return sUniqueId;
+ }
+
+ // 2. 针对 Android 10 (API 29) 及以上版本的处理
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ // 高版本直接使用 ANDROID_ID
+ sUniqueId = getAndroidId(appContext);
+ } else {
+ // 3. 针对 Android 10 以下版本的处理:尝试获取 IMEI
+ sUniqueId = getIMEI(appContext);
+ if (TextUtils.isEmpty(sUniqueId)) {
+ sUniqueId = getAndroidId(appContext);
+ }
+ }
+
+ // 4. 极端兜底:如果 ANDROID_ID 也为空(极少见)或格式错误,生成一个随机 UUID
+ if (TextUtils.isEmpty(sUniqueId) || "9774d56d6824155a".equals(sUniqueId)) {
+ sUniqueId = UUID.randomUUID().toString().replace("-", "");
+ }
+
+ // 5. 将生成的唯一标识持久化到本地,防止应用卸载重装或系统升级后发生改变
+ saveIdToFile(appContext, sUniqueId);
+
+ return sUniqueId;
+ }
+
+ /**
+ * 获取 ANDROID_ID
+ * 优点:Android 10+ 不需要任何权限
+ * 缺点:设备恢复出厂设置会改变;不同签名的 App 获取到的值可能不同
+ */
+ @SuppressLint("HardwareIds")
+ private static String getAndroidId(Context context) {
+ try {
+ String androidId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
+ if (!TextUtils.isEmpty(androidId)) {
+ return androidId;
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return "";
+ }
+
+ /**
+ * 获取 IMEI (仅在 Android 10 以下有效,且需要 READ_PHONE_STATE 权限)
+ */
+ @SuppressLint({"HardwareIds", "MissingPermission"})
+ private static String getIMEI(Context context) {
+ try {
+ TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ if (tm != null) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ return tm.getImei();
+ } else {
+ return tm.getDeviceId();
+ }
+ }
+ } catch (Exception e) {
+ // 权限未授予或 Android 10+ 抛出异常,直接捕获
+ }
+ return "";
+ }
+
+ /**
+ * 从内部存储读取缓存的 ID
+ */
+ private static String readIdFromFile(Context context) {
+ File file = new File(context.getFilesDir(), CACHE_FILE_NAME);
+ if (!file.exists()) return null;
+
+ try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
+ return reader.readLine();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ /**
+ * 将 ID 保存到内部存储
+ */
+ private static void saveIdToFile(Context context, String id) {
+ File file = new File(context.getFilesDir(), CACHE_FILE_NAME);
+ try (FileOutputStream fos = new FileOutputStream(file)) {
+ fos.write(id.getBytes());
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
}
diff --git a/app/src/main/res/layout/activity_display.xml b/app/src/main/res/layout/activity_display.xml
new file mode 100644
index 0000000..c95b89b
--- /dev/null
+++ b/app/src/main/res/layout/activity_display.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 72fd377..b9e144a 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -15,13 +15,36 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
+
+
+
+
+ app:layout_constraintTop_toBottomOf="@+id/bt_connect">
+