From 61ce9e8abe85990130a202cc769a18257c238216 Mon Sep 17 00:00:00 2001 From: tongtongstudio Date: Tue, 2 Jun 2026 20:51:37 +0800 Subject: [PATCH] =?UTF-8?q?build:=20=E6=9B=B4=E6=96=B0=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E8=87=B3studio=20panda?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 35 +- app/src/main/AndroidManifest.xml | 9 +- .../activity/main/MainActivity.java | 489 ++++++++++++++-- .../remoteclient/bean/SignalMessage.java | 9 + .../remoteclient/config/WebSocketConfig.java | 18 + .../ttstd/remoteclient/gson/GsonUtils.java | 153 +++++ .../gson/IntegerDefault0Adapter.java | 35 ++ .../gson/NullStringToEmptyAdapterFactory.java | 45 ++ .../manager/SignalingWebSocketClient.java | 237 ++++++++ .../remoteclient/service/ControlService.java | 41 -- .../service/ScreenCaptureService.java | 548 ------------------ .../ttstd/remoteclient/utils/SystemUtils.java | 29 + app/src/main/res/layout/activity_main.xml | 61 +- app/src/main/res/values/strings.xml | 2 +- app/src/main/res/values/styles.xml | 8 + build.gradle | 8 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 17 files changed, 1069 insertions(+), 660 deletions(-) create mode 100644 app/src/main/java/com/ttstd/remoteclient/bean/SignalMessage.java create mode 100644 app/src/main/java/com/ttstd/remoteclient/config/WebSocketConfig.java create mode 100644 app/src/main/java/com/ttstd/remoteclient/gson/GsonUtils.java create mode 100644 app/src/main/java/com/ttstd/remoteclient/gson/IntegerDefault0Adapter.java create mode 100644 app/src/main/java/com/ttstd/remoteclient/gson/NullStringToEmptyAdapterFactory.java create mode 100644 app/src/main/java/com/ttstd/remoteclient/manager/SignalingWebSocketClient.java delete mode 100644 app/src/main/java/com/ttstd/remoteclient/service/ControlService.java delete mode 100644 app/src/main/java/com/ttstd/remoteclient/service/ScreenCaptureService.java diff --git a/app/build.gradle b/app/build.gradle index 7396058..2c442e7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,34 +1,27 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 29 + namespace "com.ttstd.remoteclient" + + compileSdkVersion 33 // buildToolsVersion "36.0.0" defaultConfig { applicationId "com.ttstd.remoteclient" minSdkVersion 24 - targetSdkVersion 29 + targetSdkVersion 33 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - ndk { //根据需要 自行选择添加的对应cpu类型的.so库。 abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' // 还可以添加 'armeabi', 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64', 'mips', 'mips64' } - dataBinding { - enabled true - } - javaCompileOptions { annotationProcessorOptions { arguments = [AROUTER_MODULE_NAME: project.getName()] @@ -37,6 +30,16 @@ android { } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + buildFeatures { + dataBinding true + buildConfig true + } + buildTypes { release { minifyEnabled false @@ -49,6 +52,14 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) + // 添加 Kotlin 标准库 + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + // 添加这行,使用 BOM 统一 Kotlin 相关库的版本 + implementation(platform("org.jetbrains.kotlin:kotlin-bom:$kotlin_version")) + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-rx3:1.11.0' + implementation 'androidx.appcompat:appcompat:1.3.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' // For control over item selection of both touch and mouse driven selection @@ -101,7 +112,7 @@ dependencies { implementation 'com.jeremyliao:live-event-bus-x:1.7.3' implementation 'org.webrtc:google-webrtc:1.0.32006' - implementation 'org.java-websocket:Java-WebSocket:1.5.3' +// implementation 'org.java-websocket:Java-WebSocket:1.5.3' //MMKV implementation 'com.tencent:mmkv-static:1.2.14' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index af1bb7c..53a9889 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -26,6 +26,7 @@ android:theme="@style/AppTheme"> @@ -34,14 +35,6 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/java/com/ttstd/remoteclient/activity/main/MainActivity.java b/app/src/main/java/com/ttstd/remoteclient/activity/main/MainActivity.java index 865df3d..a564554 100644 --- a/app/src/main/java/com/ttstd/remoteclient/activity/main/MainActivity.java +++ b/app/src/main/java/com/ttstd/remoteclient/activity/main/MainActivity.java @@ -1,23 +1,64 @@ package com.ttstd.remoteclient.activity.main; -import android.content.Intent; -import android.media.projection.MediaProjectionManager; +import android.Manifest; +import android.content.pm.PackageManager; import android.os.Build; +import android.text.Editable; +import android.text.TextUtils; import android.util.Log; import android.view.View; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.core.app.ActivityCompat; +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.databinding.ActivityMainBinding; -import com.ttstd.remoteclient.service.ControlService; -import com.ttstd.remoteclient.service.ScreenCaptureService; +import com.ttstd.remoteclient.gson.GsonUtils; +import com.ttstd.remoteclient.manager.SignalingWebSocketClient; +import com.ttstd.remoteclient.utils.SystemUtils; -public class MainActivity extends BaseMvvmActivity { - private static final String TAG ="MainActivity"; +import org.webrtc.DataChannel; +import org.webrtc.DefaultVideoDecoderFactory; +import org.webrtc.DefaultVideoEncoderFactory; +import org.webrtc.EglBase; +import org.webrtc.IceCandidate; +import org.webrtc.MediaConstraints; +import org.webrtc.MediaStream; +import org.webrtc.PeerConnection; +import org.webrtc.PeerConnectionFactory; +import org.webrtc.RtpReceiver; +import org.webrtc.SdpObserver; +import org.webrtc.SessionDescription; +import org.webrtc.VideoTrack; - private static final int REQUEST_CODE_SCREEN_CAPTURE = 7897; - private MediaProjectionManager mMediaProjectionManager; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +public class MainActivity extends BaseMvvmActivity implements SignalingWebSocketClient.SignalingListener { + private static final String TAG = "MainActivity"; + + private static final int NETWORK_REQUEST_CODE = 1001; + + // ICE服务器配置(必须,用于NAT穿透,公共免费ICE服务器) + private static final List ICE_SERVERS = new ArrayList() {{ + add(PeerConnection.IceServer.builder("stun:stun.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; @Override public boolean setNightMode() { @@ -39,8 +80,8 @@ public class MainActivity extends BaseMvvmActivity= Build.VERSION_CODES.O) { - startForegroundService(screenServiceIntent); - }else { - startService(screenServiceIntent); + 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) { + initWebRTC(); + } else { + Toast.makeText(this, "权限被拒绝,无法使用屏幕共享功能", Toast.LENGTH_SHORT).show(); } - - // 启动指令接收服务 - Intent controlServiceIntent = new Intent(this, ControlService.class); - startService(controlServiceIntent); - } else { - Log.e(TAG, "屏幕采集权限申请失败"); } } + // 权限申请逻辑 + 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) { + ActivityCompat.requestPermissions(this, permissions, NETWORK_REQUEST_CODE); + } else { + initWebRTC(); + } + } else { + initWebRTC(); + } + } + + // ======================== 核心:初始化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(); + } + // 释放P2P连接 + if (peerConnection != null) { + peerConnection.close(); + peerConnection.dispose(); + } + // 释放工厂 + if (peerConnectionFactory != null) { + peerConnectionFactory.dispose(); + } + // 释放EGL环境 + if (rootEglBase != null) { + rootEglBase.release(); + } + + if (signalingClient != null) { + signalingClient.disconnect(); + } + } + + + // 实现 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()); + + runOnUiThread(() -> { + updateStatus("已连接,会话ID: " + sessionId); + addMessage("系统: 连接到信令服务器"); + }); + } + + @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); + } + runOnUiThread(() -> { + addMessage("收到[" + msgType + "]: " + data); + }); + } + + @Override + public void onDisconnected(String reason) { + Log.e(TAG, "onDisconnected: "); + runOnUiThread(() -> { + updateStatus("已断开: " + reason); + addMessage("系统: 连接断开 - " + reason); + }); + } + + @Override + public void onError(String error) { + Log.e(TAG, "onError: " + error); + runOnUiThread(() -> { + Log.e(TAG, "WebSocket错误: " + error); + addMessage("错误: " + error); + }); + } + + private void updateStatus(String status) { + mViewDataBinding.tvStatus.setText("状态: " + status); + } + + private void addMessage(String message) { + String current = mViewDataBinding.tvStatus.getText().toString(); + mViewDataBinding.tvStatus.setText(current + "\n" + message); + } + + public class BtnClick { - public void requestPermission(View view){ - // 适配Android 13+ 通知权限 -// if (Build.VERSION.SDK_INT >= 33) { -// if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { -// ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.POST_NOTIFICATIONS}, 1002); -// } -// } - // 申请屏幕采集权限 - Intent captureIntent = mMediaProjectionManager.createScreenCaptureIntent(); - startActivityForResult(captureIntent, REQUEST_CODE_SCREEN_CAPTURE); + 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("正在连接..."); } } diff --git a/app/src/main/java/com/ttstd/remoteclient/bean/SignalMessage.java b/app/src/main/java/com/ttstd/remoteclient/bean/SignalMessage.java new file mode 100644 index 0000000..d1a9695 --- /dev/null +++ b/app/src/main/java/com/ttstd/remoteclient/bean/SignalMessage.java @@ -0,0 +1,9 @@ +package com.ttstd.remoteclient.bean; + +import java.io.Serializable; + +public class SignalMessage implements Serializable { + + + +} diff --git a/app/src/main/java/com/ttstd/remoteclient/config/WebSocketConfig.java b/app/src/main/java/com/ttstd/remoteclient/config/WebSocketConfig.java new file mode 100644 index 0000000..06d0e7d --- /dev/null +++ b/app/src/main/java/com/ttstd/remoteclient/config/WebSocketConfig.java @@ -0,0 +1,18 @@ +package com.ttstd.remoteclient.config; + +/** + * WebSocket 配置类 + * 用于配置WebSocket连接的各种参数 + */ +public class WebSocketConfig { + public String wsUrl; // WebSocket服务器地址 + public static final long heartbeatInterval = 30000; // 心跳间隔,默认10秒 + public static final long reconnectInterval = 5000; // 重连间隔,默认5秒 + public static final int maxReconnectAttempts = 5; // 最大重连次数 + public static final boolean autoReconnect = true; // 是否自动重连 + public static final boolean needHeartbeat = true; // 是否需要心跳检测 + public static final long connectTimeout = 10; // 连接超时时间(秒) + public static final long readTimeout = 10; // 读取超时时间(秒) + public static final long writeTimeout = 10; // 写入超时时间(秒) + +} diff --git a/app/src/main/java/com/ttstd/remoteclient/gson/GsonUtils.java b/app/src/main/java/com/ttstd/remoteclient/gson/GsonUtils.java new file mode 100644 index 0000000..bcb2aa9 --- /dev/null +++ b/app/src/main/java/com/ttstd/remoteclient/gson/GsonUtils.java @@ -0,0 +1,153 @@ +package com.ttstd.remoteclient.gson; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.reflect.TypeToken; + +import java.lang.reflect.Type; +import java.util.List; +import java.util.Map; +import java.util.Objects; + + +public class GsonUtils { + //https://blog.csdn.net/zte1055889498/article/details/122400299 + + public static JsonObject getJsonObject(String jsonString) { + JsonObject jsonObject = JsonParser.parseString(jsonString).getAsJsonObject(); + return jsonObject; + } + + private static final Gson gson; + private static final Gson exposeGson; + + static { + GsonBuilder builder = new GsonBuilder(); + builder.registerTypeAdapterFactory(new NullStringToEmptyAdapterFactory()); + builder.registerTypeAdapter(Integer.class, new IntegerDefault0Adapter()); + builder.registerTypeAdapter(int.class, new IntegerDefault0Adapter()); + builder.disableHtmlEscaping(); + builder.enableComplexMapKeySerialization(); + // builder.excludeFieldsWithoutExposeAnnotation(); + builder.setDateFormat("yyyy-MM-dd HH:mm:ss"); + gson = builder.create(); + exposeGson = new GsonBuilder() + .excludeFieldsWithoutExposeAnnotation() + .create(); + } + + public static Type makeJavaType(Type rawType, Type... typeArguments) { + return TypeToken.getParameterized(rawType, typeArguments).getType(); + } + + public static String toString(Object value) { + if (Objects.isNull(value)) { + return null; + } + if (value instanceof String) { + return (String) value; + } + return toJSONString(value); + } + + public static String toJSONString(Object value) { + return gson.toJson(value); + } + + public static String toExposeJSONString(Object value) { + return gson.toJson(value); + } + + + public static String toPrettyString(Object value) { + return gson.newBuilder().setPrettyPrinting().create().toJson(value); + } + + public static JsonElement fromJavaObject(Object value) { + JsonElement result = null; + if (Objects.nonNull(value) && (value instanceof String)) { + result = parseObject((String) value); + } else { + result = gson.toJsonTree(value); + } + return result; + } + + public static JsonElement parseObject(String content) { + return JsonParser.parseString(content); + } + + public static JsonElement getJsonElement(JsonObject node, String name) { + return node.get(name); + } + + public static JsonElement getJsonElement(JsonArray node, int index) { + return node.get(index); + } + + public static T toJavaObject(JsonElement node, Class clazz) { + return gson.fromJson(node, clazz); + } + + public static T toJavaObject(JsonElement node, Type type) { + return gson.fromJson(node, type); + } + + public static T toJavaObject(JsonElement node, TypeToken typeToken) { + return toJavaObject(node, typeToken.getType()); + } + + public static List toJavaList(JsonElement node, Class clazz) { + return toJavaObject(node, makeJavaType(List.class, clazz)); + } + + public static List toJavaList(JsonElement node) { + return toJavaObject(node, new TypeToken>() { + }.getType()); + } + + public static Map toJavaMap(JsonElement node, Class clazz) { + return toJavaObject(node, makeJavaType(Map.class, String.class, clazz)); + } + + public static Map toJavaMap(JsonElement node) { + return toJavaObject(node, new TypeToken>() { + }.getType()); + } + + public static T toJavaObject(String content, Class clazz) { + JsonObject jsonObject = getJsonObject(content); + String jsonString = jsonObject.toString(); + return gson.fromJson(jsonString, clazz); + } + + public static T toJavaObject(String content, Type type) { + return gson.fromJson(content, type); + } + + public static T toJavaObject(String content, TypeToken typeToken) { + return toJavaObject(content, typeToken.getType()); + } + + public static List toJavaList(String content, Class clazz) { + return toJavaObject(content, makeJavaType(List.class, clazz)); + } + + public static List toJavaList(String content) { + return toJavaObject(content, new TypeToken>() { + }.getType()); + } + + public static Map toJavaMap(String content, Class clazz) { + return toJavaObject(content, makeJavaType(Map.class, String.class, clazz)); + } + + public static Map toJavaMap(String content) { + return toJavaObject(content, new TypeToken>() { + }.getType()); + } +} diff --git a/app/src/main/java/com/ttstd/remoteclient/gson/IntegerDefault0Adapter.java b/app/src/main/java/com/ttstd/remoteclient/gson/IntegerDefault0Adapter.java new file mode 100644 index 0000000..b735a36 --- /dev/null +++ b/app/src/main/java/com/ttstd/remoteclient/gson/IntegerDefault0Adapter.java @@ -0,0 +1,35 @@ +package com.ttstd.remoteclient.gson; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.JsonSyntaxException; + +import java.lang.reflect.Type; + +public class IntegerDefault0Adapter implements JsonSerializer, JsonDeserializer { + @Override + public Integer deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + try { + if (json.getAsString().equals("")) { + return 0; + } + } catch (Exception ignore) { + } + try { + return json.getAsInt(); + } catch (NumberFormatException e) { + throw new JsonSyntaxException(e); + } + } + + @Override + public JsonElement serialize(Integer src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ttstd/remoteclient/gson/NullStringToEmptyAdapterFactory.java b/app/src/main/java/com/ttstd/remoteclient/gson/NullStringToEmptyAdapterFactory.java new file mode 100644 index 0000000..a4f01d8 --- /dev/null +++ b/app/src/main/java/com/ttstd/remoteclient/gson/NullStringToEmptyAdapterFactory.java @@ -0,0 +1,45 @@ +package com.ttstd.remoteclient.gson; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; + +public class NullStringToEmptyAdapterFactory implements TypeAdapterFactory { + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + + Class rawType = (Class) type.getRawType(); + if (rawType != String.class) { + return null; + } + return (TypeAdapter) new StringAdapter(); + } + + public static class StringAdapter extends TypeAdapter { + @Override + public String read(JsonReader reader) throws IOException { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull(); + return ""; + } + return reader.nextString(); + } + + @Override + public void write(JsonWriter writer, String value) throws IOException { + if (value == null) { + writer.nullValue(); + return; + } + writer.value(value); + } + } + +} + diff --git a/app/src/main/java/com/ttstd/remoteclient/manager/SignalingWebSocketClient.java b/app/src/main/java/com/ttstd/remoteclient/manager/SignalingWebSocketClient.java new file mode 100644 index 0000000..41c3612 --- /dev/null +++ b/app/src/main/java/com/ttstd/remoteclient/manager/SignalingWebSocketClient.java @@ -0,0 +1,237 @@ +package com.ttstd.remoteclient.manager; + +import android.os.Handler; +import android.os.Looper; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.ttstd.remoteclient.utils.SystemUtils; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.TimeUnit; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; +import okio.ByteString; + +public class SignalingWebSocketClient { + private static final String TAG = "SignalingWebSocket"; + + public interface SignalingListener { + void onConnected(String sessionId); + + void onMessageReceived(String type, String data); + + void onDisconnected(String reason); + + void onError(String error); + } + +// private static final String SERVER_URL = "ws://175.178.213.60:2310/signaling/"; + private static final String SERVER_URL = "ws://192.168.100.111:2310/signaling/"; + + private static final int RECONNECT_DELAY_MS = 3000; + + private Gson gson = new Gson(); + private OkHttpClient client; + private WebSocket webSocket; + private Request request; + + private SignalingListener listener; + private Handler mainHandler; + private boolean mConnected = false; + private boolean shouldReconnect = true; + + private String mUserId; + + public SignalingWebSocketClient(SignalingListener listener) { + this.listener = listener; + this.mainHandler = new Handler(Looper.getMainLooper()); + + // 创建OkHttpClient,支持WebSocket + client = new OkHttpClient.Builder() + .pingInterval(20, TimeUnit.SECONDS) // 保持连接活跃 + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) + .writeTimeout(10, TimeUnit.SECONDS) + .build(); + } + + public void setUserId(String userId) { + mUserId = userId; + } + + public void connect() { + if (mConnected) { + return; + } + if (webSocket != null) { + return; + } + + request = new Request.Builder() + .url(SERVER_URL + SystemUtils.getSerial()) + .build(); + + mConnected = false; + shouldReconnect = true; + + webSocket = client.newWebSocket(request, new WebSocketListener() { + @Override + public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) { + mConnected = true; + runOnUiThread(() -> { + if (listener != null) { + listener.onConnected("连接成功"); + } + }); + } + + @Override + public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) { + runOnUiThread(() -> { + try { + JsonObject json = gson.fromJson(text, JsonObject.class); + String type = json.get("type").getAsString(); + + switch (type) { + case "connected": + String sessionId = json.get("sessionId").getAsString(); + if (listener != null) { + listener.onConnected(sessionId); + } + break; + case "signal": + String from = json.get("from").getAsString(); + String data = json.get("data").toString(); + if (listener != null) { + listener.onMessageReceived("signal", "来自" + from + ": " + data); + } + break; + case "ack": + if (listener != null) { + listener.onMessageReceived("ack", "消息发送成功"); + } + break; + case "peer_left": + String leftSessionId = json.get("sessionId").getAsString(); + if (listener != null) { + listener.onMessageReceived("peer_left", "对端离开: " + leftSessionId); + } + break; + case "error": + String errorMsg = json.get("message").getAsString(); + if (listener != null) { + listener.onError(errorMsg); + } + break; + } + } catch (Exception e) { + if (listener != null) { + listener.onMessageReceived("raw", text); + } + } + }); + } + + @Override + public void onMessage(@NotNull WebSocket webSocket, @NotNull ByteString bytes) { + // 处理二进制消息 + runOnUiThread(() -> { + if (listener != null) { + listener.onMessageReceived("binary", bytes.hex()); + } + }); + } + + @Override + public void onClosing(@NotNull WebSocket webSocket, int code, @NotNull String reason) { + mConnected = false; + runOnUiThread(() -> { + if (listener != null) { + listener.onDisconnected("连接关闭中: " + reason); + } + }); + } + + @Override + public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) { + mConnected = false; + webSocket = null; + + runOnUiThread(() -> { + if (listener != null) { + listener.onDisconnected("连接已关闭: " + reason); + } + }); + + // 自动重连 + if (shouldReconnect) { + mainHandler.postDelayed(() -> connect(), RECONNECT_DELAY_MS); + } + } + + @Override + public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable t, + @Nullable Response response) { + mConnected = false; + webSocket = null; + + runOnUiThread(() -> { + if (listener != null) { + listener.onError("连接失败: " + t.getMessage()); + listener.onDisconnected(t.getMessage()); + } + }); + + // 自动重连 + if (shouldReconnect) { + mainHandler.postDelayed(() -> connect(), RECONNECT_DELAY_MS); + } + } + }); + } + + public void sendSignal(String signalData) { + if (webSocket != null) { + webSocket.send(signalData); + } + } + + public void sendJsonSignal(JsonObject signalJson) { + if (webSocket != null) { + webSocket.send(gson.toJson(signalJson)); + } + } + + public void disconnect() { + mConnected = false; + shouldReconnect = false; + + if (webSocket != null) { + webSocket.close(1000, "正常断开"); + webSocket = null; + } + + if (client != null) { + client.dispatcher().executorService().shutdown(); + } + } + + public boolean isConnected() { + return webSocket != null; + } + + private void runOnUiThread(Runnable runnable) { + if (Looper.myLooper() == Looper.getMainLooper()) { + runnable.run(); + } else { + mainHandler.post(runnable); + } + } +} diff --git a/app/src/main/java/com/ttstd/remoteclient/service/ControlService.java b/app/src/main/java/com/ttstd/remoteclient/service/ControlService.java deleted file mode 100644 index 593b19d..0000000 --- a/app/src/main/java/com/ttstd/remoteclient/service/ControlService.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.ttstd.remoteclient.service; - -import android.app.Service; -import android.content.Intent; -import android.os.IBinder; - -import androidx.annotation.Nullable; - -public class ControlService extends Service { - @Nullable - @Override - public IBinder onBind(Intent intent) { - return null; - } - - @Override - public void onCreate() { - super.onCreate(); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - return super.onStartCommand(intent, flags, startId); - } - - @Override - public void onDestroy() { - super.onDestroy(); - } - - @Override - public void onLowMemory() { - super.onLowMemory(); - } - - @Override - public void onTrimMemory(int level) { - super.onTrimMemory(level); - } - -} diff --git a/app/src/main/java/com/ttstd/remoteclient/service/ScreenCaptureService.java b/app/src/main/java/com/ttstd/remoteclient/service/ScreenCaptureService.java deleted file mode 100644 index 0a0390a..0000000 --- a/app/src/main/java/com/ttstd/remoteclient/service/ScreenCaptureService.java +++ /dev/null @@ -1,548 +0,0 @@ -package com.ttstd.remoteclient.service; - -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.ImageFormat; -import android.graphics.Matrix; -import android.graphics.PixelFormat; -import android.graphics.Rect; -import android.graphics.YuvImage; -import android.hardware.display.VirtualDisplay; -import android.media.Image; -import android.media.ImageReader; -import android.media.MediaCodec; -import android.media.MediaCodecInfo; -import android.media.MediaFormat; -import android.media.projection.MediaProjection; -import android.media.projection.MediaProjectionManager; -import android.os.Build; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.IBinder; -import android.util.DisplayMetrics; -import android.util.Log; -import android.view.Surface; -import android.view.WindowManager; - -import androidx.annotation.Nullable; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.net.Socket; -import java.nio.ByteBuffer; - -public class ScreenCaptureService extends Service { - private static final String TAG = "ScreenCaptureService"; - - private static final int NOTIFICATION_ID = 7897; // 前台服务通知ID - private static final String CHANNEL_ID = "SCREEN_CAPTURE_CHANNEL"; // 通知渠道ID - private static final String CHANNEL_NAME = "屏幕采集服务"; // 通知渠道名称 - - - // 编码参数配置 - private static final String MIME_TYPE = "video/avc"; // H.264编码 - - private static final int BIT_RATE = 2 * 1024 * 1024; // 码率2Mbps - - private static final int FRAME_RATE = 15; // 帧率15fps - - private static final int I_FRAME_INTERVAL = 5; // 关键帧间隔5秒 - - private static final String CONTROL_HOST = "192.168.1.100"; // 控制端IP(需替换为实际IP) - private static final int CONTROL_PORT = 9999; // 控制端视频接收端口 - - private int mScreenWidth, mScreenHeight, mScreenDensity; - - private MediaProjection mediaProjection; - private MediaCodec mediaCodec; - private Surface encodeSurface; - private VirtualDisplay virtualDisplay; - private Socket videoSocket; - private OutputStream outputStream; - - private boolean isEncoding = false; - - // 采集参数 - private static final int IMAGE_FORMAT = ImageFormat.YUV_420_888; // 兼容大部分设备的格式 - - private ImageReader imageReader; - private Handler captureHandler; // 帧回调处理线程 - private boolean isCapturing = false; - private VirtualDisplay picVirtualDisplay; - - // 用于存储提取的单帧Bitmap(可根据需求改为回调/文件存储) - private Bitmap currentFrameBitmap; - - - @Nullable - @Override - public IBinder onBind(Intent intent) { - return null; - } - - @Override - public void onCreate() { - super.onCreate(); - WindowManager windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE); - DisplayMetrics metrics = new DisplayMetrics(); - windowManager.getDefaultDisplay().getRealMetrics(metrics); // 获取真实屏幕尺寸(包含状态栏等) - mScreenWidth = metrics.widthPixels; - mScreenHeight = metrics.heightPixels; - mScreenDensity = metrics.densityDpi; - Log.e(TAG, "onCreate: mScreenWidth = " + mScreenWidth); - Log.e(TAG, "onCreate: mScreenHeight = " + mScreenHeight); - Log.e(TAG, "onCreate: mScreenDensity = " + mScreenDensity); - - Log.e(TAG, "onCreate: densityDpi = " + getResources().getDisplayMetrics().densityDpi); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - createForegroundNotification(); - - if (intent != null) { - int resultCode = intent.getIntExtra("RESULT_CODE", -1); - Intent data = intent.getParcelableExtra("DATA"); - - // 获取MediaProjection实例(屏幕采集核心) - MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE); - mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data); - - if (mediaProjection != null) { - Log.d(TAG, "MediaProjection初始化成功,开始屏幕采集"); - // 此处可扩展:添加VirtualDisplay + MediaCodec编码屏幕画面为H.264 - // 并通过Socket将编码后的视频流发送到控制端 - // 初始化编码器并开始采集 -// new Thread(this::initEncoderAndCapture).start(); - - // 初始化ImageReader并开始采集 - initImageReaderAndCapture(); - - } else { - Log.e(TAG, "MediaProjection初始化失败"); - stopSelf(); - } - } - return START_STICKY; - } - - /** - * 初始化MediaCodec编码器,并创建虚拟显示采集屏幕 - */ - private void initEncoderAndCapture() { - try { - // 1. 初始化MediaCodec编码器 - MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, mScreenWidth, mScreenHeight); - format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); - format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE); - format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE); - format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, I_FRAME_INTERVAL); - format.setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline); - format.setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.AVCLevel31); - - mediaCodec = MediaCodec.createEncoderByType(MIME_TYPE); - mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); - encodeSurface = mediaCodec.createInputSurface(); // 获取编码输入Surface - mediaCodec.start(); - isEncoding = true; - Log.d(TAG, "MediaCodec编码器初始化成功"); - - // 1. 创建ImageReader,用于读取屏幕帧 - imageReader = ImageReader.newInstance( - mScreenWidth, - mScreenHeight, - IMAGE_FORMAT, - 2 // 缓冲区数量:2帧(避免帧丢失) - ); - - // 2. 创建虚拟显示,将屏幕画面投射到编码Surface - virtualDisplay = mediaProjection.createVirtualDisplay( - "ScreenCaptureDisplay", - mScreenWidth, - mScreenHeight, - mScreenDensity, - android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, - encodeSurface, - null, // 显示回调(可选) - null // 回调处理线程(可选) - ); - Log.d(TAG, "虚拟显示创建成功,开始屏幕采集"); - - // 3. 连接控制端并发送编码数据 - connectToControlServer(); - // 4. 循环读取编码后的数据并发送 - readAndSendEncodedData(); - - } catch (Exception e) { - Log.e(TAG, "编码器初始化失败", e); - stopEncoding(); - } - } - - /** - * 连接控制端视频接收服务 - */ - private void connectToControlServer() { - try { - videoSocket = new Socket(CONTROL_HOST, CONTROL_PORT); - outputStream = videoSocket.getOutputStream(); - Log.d(TAG, "已连接到控制端:" + CONTROL_HOST + ":" + CONTROL_PORT); - } catch (IOException e) { - Log.e(TAG, "连接控制端失败", e); - stopEncoding(); - } - } - - /** - * 循环读取编码后的H.264数据,并发送到控制端 - */ - private void readAndSendEncodedData() { - MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); - while (isEncoding) { - // 获取编码后的数据包索引 - int outputBufferId = mediaCodec.dequeueOutputBuffer(bufferInfo, 10000); // 10秒超时 - if (outputBufferId == MediaCodec.INFO_TRY_AGAIN_LATER) { - // 暂无数据,继续等待 - continue; - } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { - // 格式变更(如SPS/PPS数据),获取并发送 - MediaFormat newFormat = mediaCodec.getOutputFormat(); - Log.d(TAG, "编码器格式变更:" + newFormat); - sendSpsPps(newFormat); - } else if (outputBufferId >= 0) { - // 处理编码后的视频数据 - ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(outputBufferId); - if (outputBuffer != null && bufferInfo.size > 0) { - // 封装NALU(添加00 00 00 01起始码) - byte[] naluData = new byte[bufferInfo.size + 4]; - naluData[0] = 0; - naluData[1] = 0; - naluData[2] = 0; - naluData[3] = 1; - outputBuffer.get(naluData, 4, bufferInfo.size); - outputBuffer.clear(); - - // 发送数据到控制端 - if (outputStream != null) { - try { - outputStream.write(naluData); - outputStream.flush(); - } catch (IOException e) { - Log.e(TAG, "发送视频数据失败", e); - break; - } - } - } - // 释放缓冲区 - mediaCodec.releaseOutputBuffer(outputBufferId, false); - - // 检查是否结束 - if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { - Log.d(TAG, "编码结束"); - break; - } - } - } - } - - /** - * 发送SPS/PPS数据(H.264解码必需的配置数据) - */ - private void sendSpsPps(MediaFormat format) { - try { - byte[] sps = format.getByteBuffer("csd-0").array(); - byte[] pps = format.getByteBuffer("csd-1").array(); - - // 封装并发送SPS - byte[] spsData = new byte[sps.length + 4]; - spsData[0] = 0; - spsData[1] = 0; - spsData[2] = 0; - spsData[3] = 1; - System.arraycopy(sps, 0, spsData, 4, sps.length); - outputStream.write(spsData); - - // 封装并发送PPS - byte[] ppsData = new byte[pps.length + 4]; - ppsData[0] = 0; - ppsData[1] = 0; - ppsData[2] = 0; - ppsData[3] = 1; - System.arraycopy(pps, 0, ppsData, 4, pps.length); - outputStream.write(ppsData); - - Log.d(TAG, "已发送SPS/PPS配置数据"); - } catch (Exception e) { - Log.e(TAG, "发送SPS/PPS失败", e); - } - } - - /** - * 初始化ImageReader,创建虚拟显示采集屏幕帧 - */ - private void initImageReaderAndCapture() { - // 1. 创建ImageReader,用于读取屏幕帧 - imageReader = ImageReader.newInstance( - mScreenWidth, - mScreenHeight, - PixelFormat.RGBA_8888, - 2 // 缓冲区数量:2帧(避免帧丢失) - ); - - // 2. 创建帧回调线程(避免阻塞主线程) - HandlerThread handlerThread = new HandlerThread("ScreenCaptureThread"); - handlerThread.start(); - captureHandler = new Handler(handlerThread.getLooper()); - - // 3. 设置帧可用回调 - imageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() { - @Override - public void onImageAvailable(ImageReader reader) { - Image image = reader.acquireLatestImage(); - if (image != null) { - Bitmap bitmap = imageToBitmap2(image); // 将Image转换为Bitmap -// saveBitmapToFile(bitmap); // 保存Bitmap - image.close(); // 重要:关闭Image释放资源 - } - } - }, captureHandler); - isCapturing = true; - - // 4. 创建虚拟显示,将屏幕画面投射到ImageReader的Surface - picVirtualDisplay = mediaProjection.createVirtualDisplay( - "ScreenCaptureDisplay", - mScreenWidth, - mScreenHeight, - mScreenDensity, - android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, - imageReader.getSurface(), - null, - null - ); - Log.d(TAG, "虚拟显示创建成功,开始屏幕帧采集"); - } - - /** - * 帧可用回调:每次屏幕刷新都会触发此方法 - */ - private void onImageAvailable(ImageReader reader) { - // 获取最新帧(注意:必须close(),否则缓冲区会耗尽) - try (Image image = reader.acquireLatestImage()) { - if (image == null) { - return; - } - - // 提取当前帧并转换为Bitmap - currentFrameBitmap = imageToBitmap(image); - if (currentFrameBitmap != null) { - Log.d(TAG, "成功提取一帧,Bitmap尺寸:" + currentFrameBitmap.getWidth() + "x" + currentFrameBitmap.getHeight()); - - // ========== 此处可添加Bitmap的使用逻辑 ========== - // 示例1:保存为文件 - // saveBitmapToFile(currentFrameBitmap, "/sdcard/screen_shot.png"); - // 示例2:通过网络发送 - // sendBitmapToServer(currentFrameBitmap); - // 示例3:仅提取一帧后停止采集 - // stopCapture(); - } - } catch (Exception e) { - Log.e(TAG, "提取帧失败", e); - } - } - - /** - * 将Image(YUV_420_888格式)转换为Bitmap - */ - private Bitmap imageToBitmap(Image image) { - try { - // 获取Image的YUV数据 - Image.Plane[] planes = image.getPlanes(); - ByteBuffer yBuffer = planes[0].getBuffer(); - ByteBuffer uBuffer = planes[1].getBuffer(); - ByteBuffer vBuffer = planes[2].getBuffer(); - - int ySize = yBuffer.remaining(); - int uSize = uBuffer.remaining(); - int vSize = vBuffer.remaining(); - - // 将YUV数据合并为字节数组 - byte[] yuvData = new byte[ySize + uSize + vSize]; - yBuffer.get(yuvData, 0, ySize); - vBuffer.get(yuvData, ySize, vSize); // 注意:部分设备UV顺序是VU - uBuffer.get(yuvData, ySize + vSize, uSize); - - // YUV转JPEG,再转Bitmap(兼容所有设备) - YuvImage yuvImage = new YuvImage( - yuvData, - ImageFormat.NV21, // 转换为NV21格式 - image.getWidth(), - image.getHeight(), - new int[]{planes[0].getRowStride(), 0, 0} // 行步长 - ); - - // 压缩为JPEG字节数组 - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - yuvImage.compressToJpeg(new Rect(0, 0, image.getWidth(), image.getHeight()), 100, outputStream); - byte[] jpegData = outputStream.toByteArray(); - - // JPEG字节数组转Bitmap - Bitmap bitmap = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length); - - // 修正旋转(部分设备采集的帧会旋转90度) - Matrix matrix = new Matrix(); - matrix.postRotate(90); // 根据实际情况调整旋转角度(0/90/180/270) - return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); - - } catch (Exception e) { - Log.e(TAG, "YUV转Bitmap失败", e); - return null; - } - } - - private Bitmap imageToBitmap2(Image image) { - Image.Plane[] planes = image.getPlanes(); - ByteBuffer buffer = planes[0].getBuffer(); - int pixelStride = planes[0].getPixelStride(); - int rowStride = planes[0].getRowStride(); - int rowPadding = rowStride - pixelStride * image.getWidth(); // 计算行填充 - - // 创建Bitmap,注意处理行跨度可能大于图像宽度的情况 - Bitmap bitmap = Bitmap.createBitmap( - image.getWidth() + rowPadding / pixelStride, - image.getHeight(), - Bitmap.Config.ARGB_8888 - ); - bitmap.copyPixelsFromBuffer(buffer); - - // 如果存在填充,则裁剪出原始图像区域 - if (rowPadding > 0) { - bitmap = Bitmap.createBitmap(bitmap, 0, 0, image.getWidth(), image.getHeight()); - } - return bitmap; - } - -// private void saveBitmapToFile(Bitmap bitmap) { -// // 生成唯一的文件名 -// String fileName = "Screenshot_" + System.currentTimeMillis() + ".png"; -// // 保存到公共图片目录 -// File saveFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), fileName); -// -// try (FileOutputStream out = new FileOutputStream(saveFile)) { -// bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); // PNG无损压缩 -// Toast.makeText(this, "截图已保存: " + saveFile.getAbsolutePath(), Toast.LENGTH_SHORT).show(); -// } catch (IOException e) { -// e.printStackTrace(); -// } -// } - - - /** - * 停止编码和采集 - */ - private void stopEncoding() { - isEncoding = false; - - // 停止编码器 - if (mediaCodec != null) { - try { - mediaCodec.stop(); - mediaCodec.release(); - } catch (Exception e) { - Log.e(TAG, "停止编码器失败", e); - } - mediaCodec = null; - } - - // 释放虚拟显示 - if (virtualDisplay != null) { - virtualDisplay.release(); - virtualDisplay = null; - } - - if (picVirtualDisplay != null) { - picVirtualDisplay.release(); - picVirtualDisplay = null; - } - - // 释放MediaProjection - if (mediaProjection != null) { - mediaProjection.stop(); - mediaProjection = null; - } - - // 关闭网络连接 - try { - if (outputStream != null) outputStream.close(); - if (videoSocket != null) videoSocket.close(); - } catch (IOException e) { - Log.e(TAG, "关闭网络连接失败", e); - } - - Log.d(TAG, "屏幕采集已停止"); - } - - /** - * 创建前台服务通知,并将Service设为前台服务(核心修复点) - */ - private void createForegroundNotification() { - // 第一步:创建通知渠道(Android O及以上必需) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - NotificationChannel channel = new NotificationChannel( - CHANNEL_ID, - CHANNEL_NAME, - NotificationManager.IMPORTANCE_LOW // 低优先级,避免打扰用户 - ); - NotificationManager notificationManager = getSystemService(NotificationManager.class); - notificationManager.createNotificationChannel(channel); - } - - // 第二步:构建基础通知 - Notification.Builder builder = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O - ? new Notification.Builder(this, CHANNEL_ID) - : new Notification.Builder(this); - - Notification notification = builder - .setContentTitle("远程控制服务") - .setContentText("屏幕采集服务正在运行") - .setSmallIcon(android.R.drawable.ic_media_play) // 替换为你的应用图标 - .setOngoing(true) // 设为不可取消(前台服务特性) - .build(); - - // 第三步:启动前台服务(关键:指定MediaProjection类型) - if (Build.VERSION.SDK_INT >= 31) { - // Android 12+ 必须指定前台服务类型 - startForeground(NOTIFICATION_ID, notification, android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION); - } else { - // 低版本直接启动 - startForeground(NOTIFICATION_ID, notification); - } - Log.d(TAG, "前台服务启动成功"); - } - - @Override - public void onDestroy() { - super.onDestroy(); - stopForeground(STOP_FOREGROUND_REMOVE); - if (mediaProjection != null) { - mediaProjection.stop(); - Log.d(TAG, "屏幕采集服务停止"); - } - } - - @Override - public void onLowMemory() { - super.onLowMemory(); - } - - @Override - public void onTrimMemory(int level) { - super.onTrimMemory(level); - } -} diff --git a/app/src/main/java/com/ttstd/remoteclient/utils/SystemUtils.java b/app/src/main/java/com/ttstd/remoteclient/utils/SystemUtils.java index c05bf5d..feca9e4 100644 --- a/app/src/main/java/com/ttstd/remoteclient/utils/SystemUtils.java +++ b/app/src/main/java/com/ttstd/remoteclient/utils/SystemUtils.java @@ -1,12 +1,41 @@ package com.ttstd.remoteclient.utils; +import android.annotation.SuppressLint; import android.app.ActivityManager; import android.content.Context; +import android.os.Build; +import android.util.Log; +import java.lang.reflect.Method; import java.util.List; public class SystemUtils { + /** + * 获取设备序列号 + * + * @return + */ + @SuppressLint("MissingPermission") + public static String getSerial() { + String serial = "unknow"; + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {//9.0+ + serial = Build.getSerial(); + } else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {//8.0+ + serial = Build.SERIAL; + } else {//8.0- + Class c = Class.forName("android.os.SystemProperties"); + Method get = c.getMethod("get", String.class); + serial = (String) get.invoke(c, "ro.serialno"); + } + } catch (Exception e) { + e.printStackTrace(); + Log.e("e", "读取设备序列号异常:" + e.toString()); + } + return serial; + } + public static boolean isMainProcessName(Context cxt, int pid) { String packageName = cxt.getPackageName(); ActivityManager am = (ActivityManager) cxt.getSystemService(Context.ACTIVITY_SERVICE); diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 12d61a4..72fd377 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -15,16 +15,61 @@ android:layout_width="match_parent" android:layout_height="match_parent"> -