能够获取到屏幕截图
This commit is contained in:
@@ -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" />
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user