feat: 交换信令
This commit is contained in:
@@ -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 标准库
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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. 创建PeerConnectionFactory(WebRTC核心工厂)
|
||||
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. 创建PeerConnection(P2P连接核心对象,信令+数据传输都靠它)
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package com.ttstd.remoteclient.bean;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class SignalMessage implements Serializable {
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
10
app/src/main/res/layout/activity_display.xml
Normal file
10
app/src/main/res/layout/activity_display.xml
Normal 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>
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user