feat: 交换信令

This commit is contained in:
TongTongStudio
2026-06-23 10:38:31 +08:00
parent 61ce9e8abe
commit 3be6bf448e
15 changed files with 992 additions and 647 deletions

View File

@@ -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 标准库

View File

@@ -1,16 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.ttstd.remoteclient">
<!-- 网络权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- 前台服务权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- 悬浮窗权限(可选,用于显示服务状态) -->
<uses-permission android:name="android.permission.INTERNET" /> <!-- 前台服务权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- 悬浮窗权限(可选,用于显示服务状态) -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission
android:name="android.permission.POST_NOTIFICATIONS"
@@ -24,17 +22,19 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".activity.display.DisplayActivity"
android:exported="false" />
<activity
android:name=".activity.main.MainActivity"
android:theme="@style/AppThemeFitsSystem"
android:exported="true">
android:exported="true"
android:theme="@style/AppThemeFitsSystem">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -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;
});
}
}

View File

@@ -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<MainViewModel, ActivityMainBinding> implements SignalingWebSocketClient.SignalingListener {
import okio.ByteString;
public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBinding> 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<PeerConnection.IceServer> ICE_SERVERS = new ArrayList<PeerConnection.IceServer>() {{
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<IceCandidate> mPendingIceCandidates = new ArrayList<>();
private final Gson mGson = new Gson();
@Override
public boolean setNightMode() {
@@ -80,32 +88,44 @@ public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBi
@Override
protected void initView() {
// 申请权限
requestPermissions();
}
@Override
protected void initData() {
mWebSocketManager = WebSocketManager.getInstance();
mWebSocketManager.addCallback(this);
if (mWebSocketManager.isConnected()) {
mViewDataBinding.btConnect.setEnabled(false);
} else {
mViewDataBinding.btConnect.setEnabled(true);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == NETWORK_REQUEST_CODE) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
if (grantResults.length > 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<MainViewModel, ActivityMainBi
}
}
// ======================== 核心:初始化WebRTC引擎 ========================
// ======================== 初始化WebRTC引擎 ========================
private void initWebRTC() {
Log.e(TAG, "initWebRTC: ");
// 1. 初始化EGL环境WebRTC渲染必须
rootEglBase = EglBase.create();
// 2. 初始化渲染控件
mViewDataBinding.remoteVideoView.init(rootEglBase.getEglBaseContext(), null);
mViewDataBinding.remoteVideoView.setMirror(false); // 屏幕共享不需要镜像
mViewDataBinding.remoteVideoView.setEnableHardwareScaler(true); // 硬件缩放,提升性能
// 3. 初始化PeerConnectionFactory配置
PeerConnectionFactory.InitializationOptions initOptions = PeerConnectionFactory.InitializationOptions
.builder(this)
.setEnableInternalTracer(true)
.createInitializationOptions();
PeerConnectionFactory.initialize(initOptions);
// 4. 创建PeerConnectionFactoryWebRTC核心工厂
PeerConnectionFactory.Options factoryOptions = new PeerConnectionFactory.Options();
peerConnectionFactory = PeerConnectionFactory.builder()
.setOptions(factoryOptions)
.setVideoDecoderFactory(new DefaultVideoDecoderFactory(rootEglBase.getEglBaseContext()))
.setVideoEncoderFactory(new DefaultVideoEncoderFactory(rootEglBase.getEglBaseContext(), true, true))
.createPeerConnectionFactory();
// 5. 创建PeerConnectionP2P连接核心对象信令+数据传输都靠它)
createPeerConnection();
mViewDataBinding.tvStatus.setText("WebRTC初始化完成等待信令协商...");
}
// ======================== 创建P2P连接对象 ========================
private void createPeerConnection() {
Log.e(TAG, "createPeerConnection: ");
PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(ICE_SERVERS);
peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, new PeerConnection.Observer() {
// 1. ICE候选地址收集回调收集到本地ICE地址通过信令发送给远端
@Override
public void onIceCandidate(IceCandidate iceCandidate) {
Log.e(TAG, "onIceCandidate: ");
runOnUiThread(() -> 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);
}
}
}

View File

@@ -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);

View File

@@ -1,9 +0,0 @@
package com.ttstd.remoteclient.bean;
import java.io.Serializable;
public class SignalMessage implements Serializable {
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}

View File

@@ -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<WebSocketCallback> 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;
}
}

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.display.DisplayActivity">
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -15,13 +15,36 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv_ws_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="等待连接..."
android:textColor="#000000"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/bt_connect"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:onClick="@{click::connectWebSocket}"
android:text="连接websocket"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_ws_status" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraintLayout"
android:layout_width="match_parent"
android:layout_height="80dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
app:layout_constraintTop_toBottomOf="@+id/bt_connect">
<EditText
android:id="@+id/et_number"
@@ -32,17 +55,17 @@
android:hint="输入设备识别码"
android:text="981964879"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/button"
app:layout_constraintEnd_toStartOf="@+id/bt_stream"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button"
android:id="@+id/bt_stream"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:onClick="@{click::connectWebRtc}"
android:text="连接"
android:onClick="@{click::startStream}"
android:text="开始远程"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />