目前有这样的一个需求,实现一个偷录的功能,至少要满足定时的4min录制,不随着页面的销毁而消失,参考其它的偷拍实现都不能满足当前的需求,就自己重新做了一个满足功能需求。
参考Toast的设计思路,修改WindowManager,在窗体中打开相机,给予Camera2,满足拍照和录制的功能,具体实现如下:
public class CaptureCamera2Toast {
private static final String TAG = "CaptureCamera2Toast";
private static CaptureCamera2Toast sCaptureToast2 = null;
private final Context mContext;
private View mView;
private WindowManager mWM;
private WindowManager.LayoutParams mParams;
private TextureView mTextureView;
private CameraDevice mCameraDevice;
private CameraCaptureSession mCameraCaptureSession;
private ImageReader mImageReader;
private HandlerThread mBackgroundThread;
private Handler mBackgroundHandler;
private MediaRecorder mMediaRecorder;
private Size mPreviewSize;
private Size mVideoSize;
private boolean mTakingPicture;
private static final SparseIntArray DEFAULT_ORIENTATIONS = new SparseIntArray();
private static final SparseIntArray INVERSE_ORIENTATIONS = new SparseIntArray();
private static final long VIDEOTIME = 1000 * 60 * 4;//默认录制时长最大为4min
//判断当前是否处于视频录制状态
private boolean isRecoreding;
//抓拍照片最大值
private static final int PICTURE_COUNTS = 100;
//抓拍视频最大值
private static final int VIDEO_COUNTS = 30;
//判断当前是否正在拍照和录制状态
private boolean isOpen;
/**
* error 当前相机处于工作中
*/
private int ERROR_ISOPEN = -3;
/**
* error 相机打开失败
*/
private int ERROR_CAMERA_OPEN_FAIL = -2;
/**
* error 相机断开连接
*/
private int ERROR_CAMERA_DISCONNECT = -1;
/**
* A {@link Semaphore} to prevent the app from exiting before closing the camera.
*/
private Semaphore mCameraOpenCloseLock = new Semaphore(1);
/**
* Whether the app is recording video now
*/
private boolean mIsRecordingVideo;
static {
DEFAULT_ORIENTATIONS.append(Surface.ROTATION_0, 90);
DEFAULT_ORIENTATIONS.append(Surface.ROTATION_90, 0);
DEFAULT_ORIENTATIONS.append(Surface.ROTATION_180, 270);
DEFAULT_ORIENTATIONS.append(Surface.ROTATION_270, 180);
}
static {
INVERSE_ORIENTATIONS.append(Surface.ROTATION_0, 270);
INVERSE_ORIENTATIONS.append(Surface.ROTATION_90, 180);
INVERSE_ORIENTATIONS.append(Surface.ROTATION_180, 90);
INVERSE_ORIENTATIONS.append(Surface.ROTATION_270, 0);
}
private CaptureCamera2Toast(Context context) {
this.mContext = context;
initWindowManager(mContext);
}
public static CaptureCamera2Toast getInstance(Context context) {
if (sCaptureToast2 == null) {
sCaptureToast2 = new CaptureCamera2Toast(context);
}
return sCaptureToast2;
}
private void initWindowManager(Context context) {
//加载布局:布局填充器
LayoutInflater view = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mView = view.inflate(R.layout.toast_camera2_capture, null);
mTextureView = mView.findViewById(R.id.textureView);
//初始化窗体管理器
if (mWM == null) {
mWM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
}
mParams = new WindowManager.LayoutParams();
mParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
mParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
mParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
mParams.format = PixelFormat.TRANSLUCENT;
mParams.gravity = Gravity.END | Gravity.TOP;
//mParams.type = WindowManager.LayoutParams.TYPE_TOAST;//Toast默认没有触摸功能
mParams.type = WindowManager.LayoutParams.TYPE_PRIORITY_PHONE;//占据窗体前端
mParams.setTitle("Toast");
}
private void startBackgroundThread() {
mBackgroundThread = new HandlerThread("CameraBackground");
mBackgroundThread.start();
mBackgroundHandler = new Handler(mBackgroundThread.getLooper());
}
/**
* Stops the background thread and its {@link Handler}.
*/
private void stopBackgroundThread() {
try {
if (mBackgroundThread != null) {
mBackgroundThread.quitSafely();
//mBackgroundThread.join();
mBackgroundThread = null;
mBackgroundHandler = null;
}
} catch (Exception e) {
e.printStackTrace();
}
}
public void show(boolean takingPicture) {
Log.d(TAG, "show: " + isOpen);
this.mTakingPicture = takingPicture;
if (isOpen) {
Log.d(TAG, "The camera is currently in use");
captureError(ERROR_ISOPEN);
} else {
startBackgroundThread();
mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
if (mView != null) {
if (mView.getParent() != null) {
mWM.removeView(mView);
}
mWM.addView(mView, mParams);
isOpen = true;
}
}
}
public void hide() {
isOpen = false;
Log.i(TAG, "hide: " + isOpen);
if (mView != null && mView.getParent() != null) {
mWM.removeView(mView);
}
if (null != mImageReader) {
mImageReader.close();
mImageReader = null;
}
closeCamera();
//sCaptureToast2 = null;
stopBackgroundThread();
}
private void closeCamera() {
try {
mCameraOpenCloseLock.acquire();
closePreviewSession();
if (null != mCameraDevice) {
mCameraDevice.close();
mCameraDevice = null;
}
if (null != mMediaRecorder) {
mMediaRecorder.release();
mMediaRecorder = null;
}
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted while trying to lock camera closing.");
} finally {
mCameraOpenCloseLock.release();
}
}
private void closePreviewSession() {
if (mCameraCaptureSession != null) {
mCameraCaptureSession.close();
mCameraCaptureSession = null;
}
}
private TextureView.SurfaceTextureListener mSurfaceTextureListener = new TextureView.SurfaceTextureListener() {
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
openCamera(width, height);
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
return false;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
}
};
@SuppressLint("MissingPermission")
private boolean openCamera(int width, int height) {
try {
CameraManager cameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
String[] cameraIdList = cameraManager.getCameraIdList();
if (cameraIdList.length <= 0) {
Log.e(TAG, "Camera not exist");
return false;
} else {
if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
throw new RuntimeException("Time out waiting to lock camera opening.");
}
String cameraId = cameraIdList[0];
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
if (map != null) {
mVideoSize = chooseVideoSize(map.getOutputSizes(MediaRecorder.class));
mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class),
width, height, mVideoSize);
}
configureTransform(width, height);
if (isOpen) {
cameraManager.openCamera(cameraId, mStateCallback, mBackgroundHandler);
}
}
return true;
} catch (Exception e) {
e.printStackTrace();
captureError(ERROR_CAMERA_OPEN_FAIL);
hide();
Log.e(TAG, "openCamera: " + e.toString());
return false;
}
}
private void captureError(int error) {
if (mTakingPicture) {
if (mPicturePath != null) {
mPicturePath.getPictureError(error);
}
} else {
if (mVideoPath != null) {
mVideoPath.getVideoError(error);
}
}
}
private CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(CameraDevice camera) {
Log.d(TAG, "onOpened: " + camera.getId());
if (mTakingPicture) {
initTakePicture();
} else {
initMediaRecored();
}
mCameraDevice = camera;
startPreview();
mCameraOpenCloseLock.release();
}
@Override
public void onDisconnected(CameraDevice camera) {
Log.d(TAG, "onDisconnected: " + camera.toString());
captureError(ERROR_CAMERA_DISCONNECT);
mCameraOpenCloseLock.release();
camera.close();
hide();
mCameraDevice = null;
}
@Override
public void onError(CameraDevice camera, int error) {
Log.e(TAG, "onError: " + error);
captureError(error);
mCameraOpenCloseLock.release();
camera.close();
hide();
mCameraDevice = null;
}
};
private void initTakePicture() {
mImageReader = ImageReader.newInstance(1024, 600, ImageFormat.JPEG, 1);
mImageReader.setOnImageAvailableListener(reader -> {
// 拿到拍照照片数据
Image image = reader.acquireNextImage();
ByteBuffer buffer = image.getPlanes()[0].getBuffer();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);//由缓冲区存入字节数组
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
File file = new File(Objects.requireNonNull(mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES)).getAbsolutePath(),
"picture" + System.currentTimeMillis() + ".jpeg");
Log.d(TAG, "initTakePicture: " + file.getAbsolutePath());
if (mPicturePath != null) {
mPicturePath.getPicturePath(file.getAbsolutePath());
}
try {
FileOutputStream fileOutputStream = new FileOutputStream(file);
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, fileOutputStream);
fileOutputStream.flush();
} catch (Exception e) {
e.printStackTrace();
}
}, mBackgroundHandler);
}
private void initMediaRecored() {
mMediaRecorder = new MediaRecorder();
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
String videoPath = Objects.requireNonNull(mContext.getExternalFilesDir(Environment.DIRECTORY_MOVIES)).getAbsolutePath() + "/movie" + System.currentTimeMillis() + ".mp4";
Log.d(TAG, "initMediaRecored: " + videoPath);
if (mVideoPath != null) {
mVideoPath.getVideoPath(videoPath);
}
mMediaRecorder.setOutputFile(videoPath);
mMediaRecorder.setVideoEncodingBitRate(1024 * 1024);
mMediaRecorder.setVideoFrameRate(25);
mMediaRecorder.setVideoSize(mVideoSize.getWidth(), mVideoSize.getHeight());
mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
try {
mMediaRecorder.prepare();
} catch (IOException e) {
e.printStackTrace();
}
}
//开启预览
private void startPreview() {
Log.i(TAG, "startPreview: ");
try {
closePreviewSession();
CaptureRequest.Builder captureRequest = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
if (mTextureView.isAvailable()) {
SurfaceTexture surfaceTexture = mTextureView.getSurfaceTexture();
if (surfaceTexture == null) return;
Surface previewSurface = new Surface(surfaceTexture);
List<Surface> surfaceList = new ArrayList<>();
surfaceList.add(previewSurface);
if (mTakingPicture) {
surfaceList.add(mImageReader.getSurface());
} else {
surfaceList.add(mMediaRecorder.getSurface());
}
captureRequest.addTarget(previewSurface);
mCameraDevice.createCaptureSession(surfaceList, new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
mCameraCaptureSession = session;
try {
if (isOpen) {
mCameraCaptureSession.setRepeatingRequest(captureRequest.build(), null, mBackgroundHandler);
if (mTakingPicture) {
takePicture();
} else {
startRecordingVideo();
}
}
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "onConfigured: " + e.getMessage());
}
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
}
}, mBackgroundHandler);
}
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "startPreview: " + e.getMessage());
hide();
}
}
private void takePicture() {
Log.d(TAG, "takePicture: ");
if (mCameraDevice == null)
return;
try {
// 创建拍照需要的CaptureRequest.Builder
CaptureRequest.Builder captureRequestBuilder;
captureRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
// 将imageReader的surface作为CaptureRequest.Builder的目标
captureRequestBuilder.addTarget(mImageReader.getSurface());
// 自动对焦
captureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
// 自动曝光
//captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
// 获取手机方向
//int rotation = getWindowManager().getDefaultDisplay().getRotation();
int rotation = 1;
// 根据设备方向计算设置照片的方向
captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION, DEFAULT_ORIENTATIONS.get(rotation));
//拍照
CaptureRequest mCaptureRequest = captureRequestBuilder.build();
mCameraCaptureSession.capture(mCaptureRequest, mCaptureCallback, mBackgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
private CameraCaptureSession.CaptureCallback mCaptureCallback = new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureStarted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, long timestamp, long frameNumber) {
super.onCaptureStarted(session, request, timestamp, frameNumber);
Log.d(TAG, "onCaptureStarted: ");
}
@Override
public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) {
super.onCaptureCompleted(session, request, result);
DeleteFiles.deleteLastFile(mContext, Environment.DIRECTORY_PICTURES, PICTURE_COUNTS);
Log.d(TAG, "onCaptureCompleted: ");
hide();
}
@Override
public void onCaptureFailed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureFailure failure) {
super.onCaptureFailed(session, request, failure);
Log.d(TAG, "onCaptureFailed: ");
hide();
}
};
private void startRecordingVideo() {
Log.i(TAG, "startRecordingVideo: ");
if (null == mCameraDevice || !mTextureView.isAvailable()) {
return;
}
try {
SurfaceTexture texture = mTextureView.getSurfaceTexture();
assert texture != null;
texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
CaptureRequest.Builder captureRequest = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
List<Surface> surfaces = new ArrayList<>();
// Set up Surface for the camera preview
Surface previewSurface = new Surface(texture);
surfaces.add(previewSurface);
captureRequest.addTarget(previewSurface);
// Set up Surface for the MediaRecorder
Surface recorderSurface = mMediaRecorder.getSurface();
surfaces.add(recorderSurface);
captureRequest.addTarget(recorderSurface);
// Start a capture session
// Once the session starts, we can update the UI and start recording
mCameraDevice.createCaptureSession(surfaces, new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
try {
if (isOpen) {
mCameraCaptureSession = cameraCaptureSession;
cameraCaptureSession.setRepeatingRequest(captureRequest.build(), null, mBackgroundHandler);
if (!isRecoreding()) {
setRecoreding(true);
}
mMediaRecorder.start();
mIsRecordingVideo = true;
new Handler().postDelayed(() -> {
stopRecordingVideo();
}, VIDEOTIME);
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
}
}, mBackgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
public boolean isRecoreding() {
return isRecoreding;
}
public void setRecoreding(boolean recoreding) {
isRecoreding = recoreding;
}
private void stopRecordingVideo() {
try {
if (mMediaRecorder != null && mIsRecordingVideo) {
mIsRecordingVideo = false;
mMediaRecorder.stop();
mMediaRecorder.reset();
mMediaRecorder = null;
setRecoreding(false);
}
} catch (Exception exception) {
Log.e(TAG, "stopRecordingVideo: " + exception.getMessage());
} finally {
hide();
}
}
private static Size chooseVideoSize(Size[] choices) {
for (Size size : choices) {
if (size.getWidth() == size.getHeight() * 4 / 3 && size.getWidth() <= 1080) {
return size;
}
}
Log.e(TAG, "Couldn't find any suitable video size");
return choices[choices.length - 1];
}
/**
* Configures the necessary {@link android.graphics.Matrix} transformation to `mTextureView`.
* This method should not to be called until the camera preview size is determined in
* openCamera, or until the size of `mTextureView` is fixed.
*
* @param viewWidth The width of `mTextureView`
* @param viewHeight The height of `mTextureView`
*/
private void configureTransform(int viewWidth, int viewHeight) {
if (null == mTextureView) {
return;
}
// int rotation = getWindowManager().getDefaultDisplay().getRotation();
int rotation = 1;
Matrix matrix = new Matrix();
RectF viewRect = new RectF(0, 0, viewWidth, viewHeight);
RectF bufferRect = new RectF(0, 0, viewWidth, viewHeight);
float centerX = viewRect.centerX();
float centerY = viewRect.centerY();
bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY());
matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL);
float scale = Math.max(
(float) viewHeight / viewHeight,
(float) viewWidth / viewHeight);
matrix.postScale(scale, scale, centerX, centerY);
matrix.postRotate(90 * (rotation - 2), centerX, centerY);
mTextureView.setTransform(matrix);
}
/**
* Given {@code choices} of {@code Size}s supported by a camera, chooses the smallest one whose
* width and height are at least as large as the respective requested values, and whose aspect
* ratio matches with the specified value.
*
* @param choices The list of sizes that the camera supports for the intended output class
* @param width The minimum desired width
* @param height The minimum desired height
* @param aspectRatio The aspect ratio
* @return The optimal {@code Size}, or an arbitrary one if none were big enough
*/
private static Size chooseOptimalSize(Size[] choices, int width, int height, Size aspectRatio) {
// Collect the supported resolutions that are at least as big as the preview Surface
List<Size> bigEnough = new ArrayList<>();
int w = aspectRatio.getWidth();
int h = aspectRatio.getHeight();
for (Size option : choices) {
if (option.getHeight() == option.getWidth() * h / w &&
option.getWidth() >= width && option.getHeight() >= height) {
bigEnough.add(option);
}
}
// Pick the smallest of those, assuming we found any
if (bigEnough.size() > 0) {
return Collections.min(bigEnough, new CompareSizesByArea());
} else {
Log.e(TAG, "Couldn't find any suitable preview size");
return choices[0];
}
}
static class CompareSizesByArea implements Comparator<Size> {
@Override
public int compare(Size lhs, Size rhs) {
// We cast here to ensure the multiplications won't overflow
return Long.signum((long) lhs.getWidth() * lhs.getHeight() -
(long) rhs.getWidth() * rhs.getHeight());
}
}
private PicturePath mPicturePath;
public interface PicturePath {
void getPicturePath(String path);
void getPictureError(int error);
}
public void getPicturePath(PicturePath path) {
this.mPicturePath = path;
}
private VideoPath mVideoPath;
public interface VideoPath {
void getVideoPath(String path);
void getVideoError(int error);
}
public void getVideoPath(VideoPath path) {
this.mVideoPath = path;
}
}
toast_camera2_capture.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextureView
android:id="@+id/textureView"
android:layout_width="0.1dp"
android:layout_height="0.1dp"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"/>
</RelativeLayout>
因为是自定义了窗体管理器,要悬浮窗申请权限,其他动态权限自行申请
//请求悬浮窗权限
@TargetApi(Build.VERSION_CODES.M)
private void getOverlayPermission() {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, 0);
}
使用方式比较简单,直接 通过show()方法进行显示,hide()方法进行隐藏。
网友评论