build: 更新项目至studio panda

This commit is contained in:
2026-06-02 20:51:37 +08:00
parent 0ad5607b68
commit 61ce9e8abe
17 changed files with 1069 additions and 660 deletions

View File

@@ -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'

View File

@@ -26,6 +26,7 @@
android:theme="@style/AppTheme">
<activity
android:name=".activity.main.MainActivity"
android:theme="@style/AppThemeFitsSystem"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -34,14 +35,6 @@
</intent-filter>
</activity>
<!-- 屏幕采集服务 -->
<service
android:name=".service.ScreenCaptureService"
android:foregroundServiceType="mediaProjection" />
<!-- 指令执行服务 -->
<service android:name=".service.ControlService" />
</application>
</manifest>

View File

@@ -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<MainViewModel, ActivityMainBinding> {
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<MainViewModel, ActivityMainBinding> implements SignalingWebSocketClient.SignalingListener {
private static final String TAG = "MainActivity";
private static final int NETWORK_REQUEST_CODE = 1001;
// ICE服务器配置必须用于NAT穿透公共免费ICE服务器
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: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<MainViewModel, ActivityMainBi
@Override
protected void initView() {
// 获取MediaProjection管理器
mMediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
// 申请权限
requestPermissions();
}
@Override
@@ -49,39 +90,411 @@ public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBi
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_SCREEN_CAPTURE && resultCode == RESULT_OK) {
Log.d(TAG, "屏幕采集权限申请成功");
// 启动屏幕采集服务
Intent screenServiceIntent = new Intent(this, ScreenCaptureService.class);
screenServiceIntent.putExtra("RESULT_CODE", resultCode);
screenServiceIntent.putExtra("DATA", data);
if (Build.VERSION.SDK_INT >= 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. 创建PeerConnectionFactoryWebRTC核心工厂
PeerConnectionFactory.Options factoryOptions = new PeerConnectionFactory.Options();
peerConnectionFactory = PeerConnectionFactory.builder()
.setOptions(factoryOptions)
.setVideoDecoderFactory(new DefaultVideoDecoderFactory(rootEglBase.getEglBaseContext()))
.setVideoEncoderFactory(new DefaultVideoEncoderFactory(rootEglBase.getEglBaseContext(), true, true))
.createPeerConnectionFactory();
// 5. 创建PeerConnectionP2P连接核心对象信令+数据传输都靠它)
createPeerConnection();
mViewDataBinding.tvStatus.setText("WebRTC初始化完成等待信令协商...");
}
// ======================== 创建P2P连接对象 ========================
private void createPeerConnection() {
Log.e(TAG, "createPeerConnection: ");
PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(ICE_SERVERS);
peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, new PeerConnection.Observer() {
// 1. ICE候选地址收集回调收集到本地ICE地址通过信令发送给远端
@Override
public void onIceCandidate(IceCandidate iceCandidate) {
Log.e(TAG, "onIceCandidate: ");
runOnUiThread(() -> mViewDataBinding.tvStatus.setText("收集到ICE候选地址发送给远端..."));
// TODO: 这里将 iceCandidate 转换成JSON通过你的信令服务器发送给远端设备
sendIceCandidateToRemote(iceCandidate);
}
// 2. ICE连接状态变化回调
@Override
public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
Log.e(TAG, "onIceConnectionChange: ");
runOnUiThread(() -> {
mViewDataBinding.tvStatus.setText("连接状态:" + iceConnectionState.name());
if (iceConnectionState == PeerConnection.IceConnectionState.CONNECTED) {
mViewDataBinding.tvStatus.setText("✅ 已成功连接,正在接收远程屏幕...");
} else if (iceConnectionState == PeerConnection.IceConnectionState.DISCONNECTED) {
mViewDataBinding.tvStatus.setText("❌ 连接断开");
}
});
}
// 3. 收到远端媒体流回调【核心】:收到远程屏幕流,渲染到控件上
@Override
public void onAddStream(MediaStream mediaStream) {
Log.e(TAG, "onAddStream: ");
runOnUiThread(() -> mViewDataBinding.tvStatus.setText("✅ 收到远程屏幕流,开始渲染..."));
// 获取视频轨道,添加到渲染控件
VideoTrack videoTrack = mediaStream.videoTracks.get(0);
videoTrack.addSink(mViewDataBinding.remoteVideoView);
}
// 其他生命周期回调,默认实现即可
@Override
public void onSignalingChange(PeerConnection.SignalingState signalingState) {
Log.e(TAG, "onSignalingChange: ");
}
@Override
public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
Log.e(TAG, "onIceGatheringChange: ");
}
@Override
public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {
Log.e(TAG, "onIceCandidatesRemoved: ");
}
@Override
public void onConnectionChange(PeerConnection.PeerConnectionState newState) {
Log.e(TAG, "onConnectionChange: ");
}
@Override
public void onIceConnectionReceivingChange(boolean b) {
Log.e(TAG, "onIceConnectionReceivingChange: ");
}
@Override
public void onRemoveStream(MediaStream mediaStream) {
Log.e(TAG, "onRemoveStream: ");
}
@Override
public void onDataChannel(DataChannel dataChannel) {
Log.e(TAG, "onDataChannel: ");
}
@Override
public void onRenegotiationNeeded() {
Log.e(TAG, "onRenegotiationNeeded: ");
}
@Override
public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {
Log.e(TAG, "onAddTrack: ");
}
});
}
// ======================== 核心信令交互方法(对外提供调用) ========================
/**
* 1. 收到远端发送的 SDP Offer 数据(信令协商第一步)
*
* @param sdpJson 远端发送的Offer-SDP字符串JSON格式
*/
public void onReceiveRemoteOffer(String sdpJson) {
SessionDescription offerSdp = new SessionDescription(SessionDescription.Type.OFFER, sdpJson);
peerConnection.setRemoteDescription(new SdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {
Log.e("setRemoteDescription", "onCreateSuccess: ");
}
@Override
public void onSetSuccess() {
Log.e("setRemoteDescription", "onCreateSuccess: ");
// 设置远端Offer成功生成本地Answer并发送给远端
peerConnection.createAnswer(new SdpObserver() {
@Override
public void onCreateSuccess(SessionDescription answerSdp) {
peerConnection.setLocalDescription(new SdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {
Log.e("setLocalDescription", "onCreateSuccess: ");
}
@Override
public void onSetSuccess() {
Log.e("setLocalDescription", "onSetSuccess: ");
// TODO: 将本地Answer-SDP发送给远端
sendSdpAnswerToRemote(answerSdp);
}
@Override
public void onCreateFailure(String s) {
Log.e("setLocalDescription", "onCreateFailure: " + s);
}
@Override
public void onSetFailure(String s) {
Log.e("setLocalDescription", "onSetFailure: " + s);
}
}, answerSdp);
}
@Override
public void onCreateFailure(String s) {
Log.e("createAnswer", "onCreateFailure: " + s);
}
@Override
public void onSetSuccess() {
Log.e("createAnswer", "onSetSuccess: ");
}
@Override
public void onSetFailure(String s) {
Log.e("createAnswer", "onSetFailure: " + s);
}
}, new MediaConstraints());
}
@Override
public void onCreateFailure(String s) {
Log.e("setRemoteDescription", "onCreateFailure: " + s);
}
@Override
public void onSetFailure(String s) {
Log.e("setRemoteDescription", "onSetFailure: " + s);
}
}, offerSdp);
}
/**
* 2. 收到远端发送的 ICE候选地址
*
* @param iceCandidateJson 远端的ICE地址字符串JSON格式
* @param sdpMid ICE的mid标识
* @param sdpMLineIndex ICE的索引
*/
public void onReceiveRemoteIceCandidate(String iceCandidateJson, String sdpMid, int sdpMLineIndex) {
Log.e(TAG, "onReceiveRemoteIceCandidate: ");
IceCandidate iceCandidate = new IceCandidate(sdpMid, sdpMLineIndex, iceCandidateJson);
peerConnection.addIceCandidate(iceCandidate);
}
// ======================== 信令发送的桥接方法(需要你对接自己的信令服务器) ========================
/**
* 发送本地ICE候选地址到远端
* TODO: 这里替换成你的信令逻辑WebSocket/Http长连接/MQTT等
*/
private void sendIceCandidateToRemote(IceCandidate iceCandidate) {
Log.e(TAG, "sendIceCandidateToRemote: ");
String mid = iceCandidate.sdpMid;
int index = iceCandidate.sdpMLineIndex;
String sdp = iceCandidate.sdp;
// 调用你的信令发送接口把iceJson、mid、index发给远端
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("msg_type", 4);
// 发送信令消息
JsonObject iceCandidateInfo = new JsonObject();
iceCandidateInfo.addProperty("sdpMid", mid);
iceCandidateInfo.addProperty("sdpMLineIndex", index);
iceCandidateInfo.addProperty("sdp", sdp);
iceCandidateInfo.addProperty("fromUser", SystemUtils.getSerial());
iceCandidateInfo.addProperty("toUser", mRemoteUserId);
iceCandidateInfo.addProperty("timestamp", System.currentTimeMillis());
jsonObject.add("content", iceCandidateInfo);
}
/**
* 发送本地Answer-SDP到远端
* TODO: 这里替换成你的信令逻辑
*/
private void sendSdpAnswerToRemote(SessionDescription description) {
Log.e(TAG, "sendIceCandidateToRemote: ");
// 调用你的信令发送接口把sdpAnswer发给远端
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("msg_type", 3);
// 发送信令消息
JsonObject signal = new JsonObject();
signal.addProperty("type", description.type.canonicalForm()); // 示例offer信令
signal.addProperty("sdp", description.description);
signal.addProperty("fromUser", SystemUtils.getSerial());
signal.addProperty("toUser", mRemoteUserId);
signal.addProperty("timestamp", System.currentTimeMillis());
jsonObject.add("content", signal);
signalingClient.sendSignal(jsonObject.toString());
}
// ======================== 生命周期管理 + 资源释放(必须,防止内存泄漏) ========================
@Override
protected void onDestroy() {
super.onDestroy();
// 释放渲染控件
if (mViewDataBinding.remoteVideoView != null) {
mViewDataBinding.remoteVideoView.release();
}
// 释放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("正在连接...");
}
}

View File

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

View File

@@ -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; // 写入超时时间(秒)
}

View File

@@ -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> T toJavaObject(JsonElement node, Class<T> clazz) {
return gson.fromJson(node, clazz);
}
public static <T> T toJavaObject(JsonElement node, Type type) {
return gson.fromJson(node, type);
}
public static <T> T toJavaObject(JsonElement node, TypeToken<?> typeToken) {
return toJavaObject(node, typeToken.getType());
}
public static <E> List<E> toJavaList(JsonElement node, Class<E> clazz) {
return toJavaObject(node, makeJavaType(List.class, clazz));
}
public static List<Object> toJavaList(JsonElement node) {
return toJavaObject(node, new TypeToken<List<Object>>() {
}.getType());
}
public static <V> Map<String, V> toJavaMap(JsonElement node, Class<V> clazz) {
return toJavaObject(node, makeJavaType(Map.class, String.class, clazz));
}
public static Map<String, Object> toJavaMap(JsonElement node) {
return toJavaObject(node, new TypeToken<Map<String, Object>>() {
}.getType());
}
public static <T> T toJavaObject(String content, Class<T> clazz) {
JsonObject jsonObject = getJsonObject(content);
String jsonString = jsonObject.toString();
return gson.fromJson(jsonString, clazz);
}
public static <T> T toJavaObject(String content, Type type) {
return gson.fromJson(content, type);
}
public static <T> T toJavaObject(String content, TypeToken<?> typeToken) {
return toJavaObject(content, typeToken.getType());
}
public static <E> List<E> toJavaList(String content, Class<E> clazz) {
return toJavaObject(content, makeJavaType(List.class, clazz));
}
public static List<Object> toJavaList(String content) {
return toJavaObject(content, new TypeToken<List<Object>>() {
}.getType());
}
public static <V> Map<String, V> toJavaMap(String content, Class<V> clazz) {
return toJavaObject(content, makeJavaType(Map.class, String.class, clazz));
}
public static Map<String, Object> toJavaMap(String content) {
return toJavaObject(content, new TypeToken<Map<String, Object>>() {
}.getType());
}
}

View File

@@ -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<Integer>, JsonDeserializer<Integer> {
@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);
}
}

View File

@@ -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<T> implements TypeAdapterFactory {
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
Class<T> rawType = (Class<T>) type.getRawType();
if (rawType != String.class) {
return null;
}
return (TypeAdapter<T>) new StringAdapter();
}
public static class StringAdapter extends TypeAdapter<String> {
@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);
}
}
}

View File

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

View File

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

View File

@@ -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);
}
}
/**
* 将ImageYUV_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);
}
}

View File

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

View File

@@ -15,16 +15,61 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/bt_premission"
android:layout_width="wrap_content"
<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">
<EditText
android:id="@+id/et_number"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:hint="输入设备识别码"
android:text="981964879"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:onClick="@{click::connectWebRtc}"
android:text="连接"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/tv_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{click::requestPermission}"
android:text="权限"
android:layout_gravity="bottom|center"
android:maxLines="2"
android:text="等待连接..."
android:textColor="#000000"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/constraintLayout" />
<org.webrtc.SurfaceViewRenderer
android:id="@+id/remote_video_view"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_status" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,3 +1,3 @@
<resources>
<string name="app_name">TTSTDRemoteService</string>
<string name="app_name">TTSTDRemoteClient</string>
</resources>

View File

@@ -15,4 +15,12 @@
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppThemeFitsSystem" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:fitsSystemWindows">true</item>
</style>
</resources>

View File

@@ -1,7 +1,8 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '2.3.21'
repositories {
google()
// mavenCentral()
@@ -13,8 +14,9 @@ buildscript {
maven { url 'https://maven.aliyun.com/repository/google' }
}
dependencies {
classpath 'com.android.tools.build:gradle:3.6.4'
classpath 'com.android.tools.build:gradle:8.13.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files

View File

@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip