Android实现偷拍(视频录制)功能的一种方法

2021-02-03  本文已影响0人  朝阳夕晖蜂蜜

目前有这样的一个需求,实现一个偷录的功能,至少要满足定时的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()方法进行隐藏。

上一篇 下一篇

猜你喜欢

热点阅读