From c00251ad642c13557c41e54be44315907ff4895a Mon Sep 17 00:00:00 2001 From: tongtongstudio Date: Tue, 23 Dec 2025 02:07:09 +0800 Subject: [PATCH] =?UTF-8?q?=E8=83=BD=E5=A4=9F=E8=8E=B7=E5=8F=96=E5=88=B0?= =?UTF-8?q?=E5=B1=8F=E5=B9=95=E6=88=AA=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 17 +- .../activity/main/MainActivity.java | 54 +- .../service/ScreenCaptureService.java | 510 +++++++++++++++++- app/src/main/res/layout/activity_main.xml | 10 +- 4 files changed, 581 insertions(+), 10 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2cafe2f..a849a4d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,17 +8,25 @@ + + + + - + @@ -27,7 +35,10 @@ - + + diff --git a/app/src/main/java/com/ttstd/remoteservice/activity/main/MainActivity.java b/app/src/main/java/com/ttstd/remoteservice/activity/main/MainActivity.java index ba50e7b..e4b2611 100644 --- a/app/src/main/java/com/ttstd/remoteservice/activity/main/MainActivity.java +++ b/app/src/main/java/com/ttstd/remoteservice/activity/main/MainActivity.java @@ -1,10 +1,27 @@ package com.ttstd.remoteservice.activity.main; +import android.Manifest; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.media.projection.MediaProjectionManager; +import android.os.Build; +import android.util.Log; +import android.view.View; + +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + import com.ttstd.remoteservice.R; import com.ttstd.remoteservice.base.mvvm.BaseMvvmActivity; import com.ttstd.remoteservice.databinding.ActivityMainBinding; +import com.ttstd.remoteservice.service.ControlService; +import com.ttstd.remoteservice.service.ScreenCaptureService; public class MainActivity extends BaseMvvmActivity { + private static final String TAG ="MainActivity"; + + private static final int REQUEST_CODE_SCREEN_CAPTURE = 7897; + private MediaProjectionManager mMediaProjectionManager; @Override @@ -27,7 +44,8 @@ public class MainActivity extends BaseMvvmActivity= Build.VERSION_CODES.O) { + startForegroundService(screenServiceIntent); + }else { + startService(screenServiceIntent); + } + + // 启动指令接收服务 + Intent controlServiceIntent = new Intent(this, ControlService.class); + startService(controlServiceIntent); + } else { + Log.e(TAG, "屏幕采集权限申请失败"); + } + } 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); + } } diff --git a/app/src/main/java/com/ttstd/remoteservice/service/ScreenCaptureService.java b/app/src/main/java/com/ttstd/remoteservice/service/ScreenCaptureService.java index 4da7d93..f0e1a6b 100644 --- a/app/src/main/java/com/ttstd/remoteservice/service/ScreenCaptureService.java +++ b/app/src/main/java/com/ttstd/remoteservice/service/ScreenCaptureService.java @@ -1,12 +1,86 @@ package com.ttstd.remoteservice.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) { @@ -16,16 +90,450 @@ public class ScreenCaptureService extends Service { @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) { - return super.onStartCommand(intent, flags, 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 diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 9cc1e6a..f1467b0 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -5,6 +5,7 @@ tools:context=".activity.main.MainActivity"> + @@ -14,16 +15,17 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - - + \ No newline at end of file