能够获取到屏幕截图

This commit is contained in:
2025-12-23 02:07:09 +08:00
parent b324ed71ca
commit c00251ad64
4 changed files with 581 additions and 10 deletions

View File

@@ -8,17 +8,25 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- 悬浮窗权限(可选,用于显示服务状态) -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission
android:name="android.permission.POST_NOTIFICATIONS"
android:minSdkVersion="33" />
<application
android:name=".base.BaseApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:name=".base.BaseApplication"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".activity.main.MainActivity">
<activity
android:name=".activity.main.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -27,7 +35,10 @@
</activity>
<!-- 屏幕采集服务 -->
<service android:name=".service.ScreenCaptureService" />
<service
android:name=".service.ScreenCaptureService"
android:foregroundServiceType="mediaProjection" />
<!-- 指令执行服务 -->
<service android:name=".service.ControlService" />

View File

@@ -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<MainViewModel, ActivityMainBinding> {
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<MainViewModel, ActivityMainBi
@Override
protected void initView() {
// 获取MediaProjection管理器
mMediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
}
@Override
@@ -35,9 +53,41 @@ 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);
}
// 启动指令接收服务
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);
}
}

View File

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

View File

@@ -5,6 +5,7 @@
tools:context=".activity.main.MainActivity">
<data>
<variable
name="click"
type="com.ttstd.remoteservice.activity.main.MainActivity.BtnClick" />
@@ -14,16 +15,17 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
<Button
android:id="@+id/bt_premission"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
android:onClick="@{click::requestPermission}"
android:text="权限"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>