Lizard & Dog Blog

Mise en œuvre de Camera 2 pour Android – Java – Partie 1

Introduction:

Rencontres-tu des difficultés pour mettre en œuvre Camera2 pour Android ? Travailler avec les fonctionnalités de la caméra sur Android peut être une tâche complexe, surtout si tu débutes. Ne t’inquiète pas, dans cet article, nous te fournirons un guide étape par étape pour t’aider à configurer Camera2 dans ton application Android. En suivant nos instructions, tu pourras exploiter la puissance de l’API Camera 2 et commencer à développer des fonctionnalités basées sur la caméra pour ton application. Alors, plonge-y et exploite le potentiel de Camera2.

Camera 2 ou Camera X ?!

Visualise l’API Camera2 comme à un appareil photo avancé qui permet de tout contrôler, de la durée d’exposition à l’ISO jusqu’à la balance des blancs. C’est comme avoir un appareil photo professionnel dans ton téléphone ! Avec les inconvénients de la complexité plus avancé d’un appareil photo pro.

CameraX, quant à lui, est comme un petit appareil photo point-and-shoot qui est facile à utiliser et offre une expérience cohérente sur différents appareils Android. Il est idéal pour une utilisation quotidienne et ne nécessite pas beaucoup de connaissances techniques.

Mais voici le hic – si tu veux capturer des vidéos à grande vitesse ou des prises de vue en rafale, ou si tu veux réaliser un traitement d’image vraiment élaboré, alors tu devras utiliser les fonctionnalités avancées de l’API Camera2. C’est un peu comme avoir besoin d’utiliser un appareil photo professionnel pour capturer une action complexe ou un beau paysage.

C’est comme choisir entre un appareil photo simple et facile à utiliser ou un appareil photo professionnel et avancé.

Comme nous choisissons toujours les options les plus difficiles, nous allons évidemment opter pour Camera 2.

Objectif de ce tuto:

La première partie de ce tutoriel se concentrera sur la capture de la vidéo venant de la caméra et son affichage sur une TextureView personnalisée (AutoFitTextureView), sur laquelle tu pourras effectuer un zoom avant et arrière.

Ultérieurement, nous nous concentrerons sur la capture de l’image et discuterons des techniques de traitement d’image potentiellement applicables.

Setup:

Nous allons créer une activité Android qui implémente l’API Camera 2 ainsi qu’une vue personnalisée (AutoFitTextureView).

Pour commencer, crée un nouveau projet dans Android Studio et choisis une activité vide.

Ajoute la permission suivante à ton fichier Manifeste :

<uses-permission android:name="android.permission.CAMERA" />

Passons maintenant au code, en commençant par la view.

Code:

1. AutoFitTextureView:

L’AutoFitTextureView est une custom View qui affiche l’aperçu de la caméra dans un rapport d’aspect correspondant aux dimensions de la view, avec une fonctionnalité de zoom/dézoom.

Elle maintient le rapport des dimensions de l’image à l’aide de la méthode setAspectRatio(), qui prend deux entiers représentant la largeur et la hauteur du rapport des dimensions de l’image.

L’interface getZoomCaracteristics fournit des informations sur les capacités de zoom de la caméra, telles que la région de zoom actuelle et le niveau de zoom maximal.

La fonctionnalité de zoom utilise la classe ScaleGestureDetector pour effectuer un zoom avant et un zoom arrière en pinçant l’écran.

public class AutoFitTextureView extends TextureView {
    // Ratio of width to height of the view
    private int mRatioWidth = 0;
    private int mRatioHeight = 0;

    // Interface to get zoom characteristics from the camera
    public getZoomCaracteristics getZoomCaracteristics;

    // Boolean to keep track if it's the first time measuring
    boolean firstMeasure;

    // Rectangles to hold camera zoom area
    Rect cameraRecteub;
    public Rect cameraRecteub1;

    // Float to hold maximum zoom level
    float maxZoomTeub;

    // Integer to keep track of current zoom level
    int zoom_level;

    // Scale gesture detector to detect pinch zoom gestures
    private ScaleGestureDetector mScaleDetector;

    // Float to hold the current scale factor
    private float mScaleFactor = 3.f;

    // Boolean to keep track if it's the first time getting the max zoom level
    private boolean isfirstmaxzoomteub;

    // Constructor with single argument
    public AutoFitTextureView(Context context) {
        this(context, null);

        // Initialize some variables
        this.getZoomCaracteristics = null;
        this.firstMeasure=true;
        this.zoom_level=0;
        this.isfirstmaxzoomteub=true;

        // Create a new scale gesture detector
        mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
    }

    public AutoFitTextureView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
        this.getZoomCaracteristics = null;
        this.firstMeasure=true;
        this.zoom_level=0;
        this.isfirstmaxzoomteub=true;
        mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
    }

    public AutoFitTextureView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        this.getZoomCaracteristics = null;
        this.firstMeasure=true;
        this.zoom_level=0;
        this.isfirstmaxzoomteub=true;
        mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
    }


    // Interface to get zoom characteristics from the camera
    public interface getZoomCaracteristics{

        // Method to get the zoom rectangle from the camera
        Rect giveRectZoom() throws CameraAccessException;

        // Method to get the maximum zoom level from the camera
        float giveMaxZoom() throws CameraAccessException;

        // Method to set the zoom level for preview
        void previewRequestINT(Rect rect);

        // Method to create a capture session with the current zoom level
        void captureSession() throws CameraAccessException;

    }

    // Method to set the interface to get zoom characteristics from the camera
    public void setGetZoomCaracteristics(getZoomCaracteristics getZoomCaracteristics){
        this.getZoomCaracteristics=getZoomCaracteristics;
    }

    // Method to set the aspect ratio of the view
    public void setAspectRatio(int width, int height) {
        // Check if width and height are non-negative values, if not, throw an exception with a message
        if (width < 0 || height < 0) {
            throw new IllegalArgumentException("Size cannot be negative.");
        }
        // Assign the width and height values to the class variables
        mRatioWidth = width;
        mRatioHeight = height;
        // Request a layout to update the view based on the new aspect ratio
        requestLayout();
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // Gets the measured width and height values from the passed in widthMeasureSpec and heightMeasureSpec
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        if (0 == mRatioWidth || 0 == mRatioHeight) {
            // If either mRatioWidth or mRatioHeight is 0, then set the dimensions to the passed in values
            setMeasuredDimension(width, height);
        } else {
            // Calculate the ratio of width and height to mRatioWidth and mRatioHeight and set the dimension accordingly
            if (width < height * mRatioWidth / mRatioHeight) {
                setMeasuredDimension(width, width * mRatioHeight / mRatioWidth);
            } else {
                setMeasuredDimension(height * mRatioWidth / mRatioHeight, height);
            }
        }
        // Increments the counter variable

    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // Pass touch event to scale gesture detector
        mScaleDetector.onTouchEvent(event);
        return true;
    }


    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            // Get maximum zoom and current zoom rectangle if they have not been retrieved yet
            if (getZoomCaracteristics!=null && isfirstmaxzoomteub){
                try {
                    maxZoomTeub=  Math.min(3f,getZoomCaracteristics.giveMaxZoom());
                    isfirstmaxzoomteub=false;
                } catch (CameraAccessException e) {
                    e.printStackTrace();
                }
                try {
                    cameraRecteub=getZoomCaracteristics.giveRectZoom();
                    cameraRecteub1=getZoomCaracteristics.giveRectZoom();

                } catch (CameraAccessException e) {
                    e.printStackTrace();
                }
            }
// Get the scale factor from the gesture detector
            mScaleFactor = detector.getScaleFactor(); // Compute the eventual width after scaling
            float eventualWidth=cameraRecteub.width()/mScaleFactor;
            if (mScaleFactor>1){
                if (eventualWidth<cameraRecteub1.width()/3.0){ // If the eventual width is less than one third of the maximum width

                    cameraRecteub.set((int) ( 0.5f*(2*cameraRecteub1.width()/3.0)),
                            (int) ( 0.5f*(2*cameraRecteub1.height()/3)),
                            (int) ( 0.5f*(4*cameraRecteub1.width()/3)),
                            (int) ( 0.5f*(4*cameraRecteub1.height()/3)));

                }
                else{ // If the eventual width is greater than or equal to one third of the maximum width
                    cameraRecteub.set((int) ( 0.5f*(cameraRecteub1.width()-cameraRecteub.width()/mScaleFactor)),
                            (int) ( 0.5f*(cameraRecteub1.height()-cameraRecteub.height()/mScaleFactor)),
                            (int) ( 0.5f*(cameraRecteub1.width()+cameraRecteub.width()/mScaleFactor)),
                            (int) ( 0.5f*(cameraRecteub1.height()+cameraRecteub.height()/mScaleFactor)));
                }

            }
            else{ // If scaling down

                if (eventualWidth>cameraRecteub1.width()){
                    cameraRecteub.set(cameraRecteub1);
                }
                else{  // If the eventual width is less than or equal to the maximum width
                    cameraRecteub.set((int) ( 0.5f*(cameraRecteub1.width()-cameraRecteub.width()/mScaleFactor)),
                            (int) ( 0.5f*(cameraRecteub1.height()-cameraRecteub.height()/mScaleFactor)),
                            (int) ( 0.5f*(cameraRecteub1.width()+cameraRecteub.width()/mScaleFactor)),
                            (int) (( 0.5f*(cameraRecteub1.height()+cameraRecteub.height()/mScaleFactor))));
                }}

            if(getZoomCaracteristics!=null){
                getZoomCaracteristics.previewRequestINT(cameraRecteub);

            }
            if(getZoomCaracteristics!=null){
                try {
                    getZoomCaracteristics.captureSession();
                } catch (CameraAccessException e) {
                    e.printStackTrace();
                }}
            return true;
        }
    }

}

Tu peux également réaliser une version plus simple de cette vue en utilisant une TextureView plus basique.

Une fois le code de la vue prêt, ajoute-le à ton fichier de mise en page (layout).

Maintenant, passons à MainActivity.

2. MainActivity:

Il faut avant tout configurer les valeurs d’orientation de la caméra et d’autres paramètres importants sur lesquels je t’invite à jeter un coup d’oeil aux commentaires laissés (états de la caméra, paramètres d’enregistrement multimédia, la TextureView, l’ImageReader, le chemin du fichier, le mode flash…).

Pour référence, les définitions de la classe CameraService seront fournies ultérieurement dans cet article.

Ci-dessous, tu trouveras les définitions des variables avec différents commentaires expliquant comment chacune sera utilisée dans le contexte de notre code (ajoute-les à la classe MainActivity) :

2.1. Variables:
 // SparseIntArray that maps device orientation to degrees
    private static final SparseIntArray ORIENTATIONS = new SparseIntArray();

    // Assigning device orientation to degrees
    static {
        ORIENTATIONS.append(Surface.ROTATION_0, 90);
        ORIENTATIONS.append(Surface.ROTATION_90, 0);
        ORIENTATIONS.append(Surface.ROTATION_180, 270);
        ORIENTATIONS.append(Surface.ROTATION_270, 180);
    }

    // States of the camera state machine
    private static final int STATE_PREVIEW = 0;
    private static final int STATE_WAITING_LOCK = 1;
    private static final int STATE_WAITING_PRECAPTURE = 2;
    private static final int STATE_WAITING_NON_PRECAPTURE = 3;
    private static final int STATE_PICTURE_TAKEN = 4;

    // Maximum preview size of the camera
    private static final int MAX_PREVIEW_WIDTH = 1920;
    private static final int MAX_PREVIEW_HEIGHT = 1080;



    // Array of available camera services
    private CameraService[] cameraServiceList;

   // Index of the currently opened camera (default to zero)
    private int openedCamera=0;

    // Texture view to display the camera preview
    private AutoFitTextureView mTextureView;

    // Size of the camera preview
    private Size mPreviewSize;

    // Background thread for camera operations
    private HandlerThread mBackgroundThread;

    // Handler for the background thread
    private Handler mBackgroundHandler;

    // Image reader to capture still images
    private ImageReader mImageReader;

    // Byte array to store the captured image
    public byte[] byteArrayImage;

    // Builder for the camera preview request
    private CaptureRequest.Builder mPreviewRequestBuilder;

    // Camera preview request
    private CaptureRequest mPreviewRequest;

    // Current state of the camera state machine
    private int mState = STATE_PREVIEW;

    // Semaphore used to lock the camera while in use

    private Semaphore mCameraOpenCloseLock = new Semaphore(1);
    /*A Semaphore is a synchronization primitive in Java that is used to control access to a shared resource.
    The count of a Semaphore represents the number of permits available to access the shared resource.
    In this case, the Semaphore is being used to control access to a camera resource.
    The count of 1 means that only one thread can access the camera resource at a time,
    so this Semaphore is being used to enforce mutual exclusion and prevent multiple threads
    from accessing the camera resource simultaneously.The Semaphore will be acquired (decremented)
    when a thread requests access to the camera,
    and it will be released (incremented) when the thread is finished using the camera.*/

    // Boolean to indicate if the device's flash is supported
    private boolean mFlashSupported;

    // Orientation of the camera sensor
    private int mSensorOrientation;

    // Flash mode, 0 for off, 1 for auto, 2 for always on
    private int flashMode;

Ci-dessous la méthode OnCreate:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Classic Android, you have to find your view by ID
        mTextureView=findViewById(R.id.autoFitTextureView);


        startBackgroundThread(); // Go check below what this method does (it launches the thread to use the camera

        if (mTextureView.isAvailable()) {
            try {
                // Launch camera if the textureView is ready
                openCamera(mTextureView.getWidth(), mTextureView.getHeight());
            } catch (CameraAccessException e) {
                e.printStackTrace();
            }
        } else {
            // Wait for the texture View to be ready to launch camera
            mTextureView.setSurfaceTextureListener(autoFitTextureListener);
        }

    }

Dans la méthode onCreate, tu peux voir que nous configurons l’AutoFitTextureView pour afficher l’aperçu de la caméra. Nous démarrons un thread en arrière-plan pour utiliser la caméra et lançons la caméra si la TextureView est prête. Si la TextureView n’est pas prête, un Listener lui est attaché, comme défini ci-dessous :

 // Variable to set texture view
    private final TextureView.SurfaceTextureListener autoFitTextureListener
            = new TextureView.SurfaceTextureListener() {

        @Override
        public void onSurfaceTextureAvailable(SurfaceTexture texture, int width, int height) {

            try {
                openCamera(width, height);
            } catch (CameraAccessException e) {
                e.printStackTrace();
            }
        }
        @Override
        public void onSurfaceTextureSizeChanged(SurfaceTexture texture, int width, int height) {
            configureTransform(width, height);
        }
        @Override
        public boolean onSurfaceTextureDestroyed(SurfaceTexture texture) {
            return true;
        }
        @Override
        public void onSurfaceTextureUpdated(SurfaceTexture texture) {
        }

    }; 

Maintenant, regardons les différentes méthodes citées dans Oncreate:

2.2. openCamera Method:
    private void openCamera(int width, int height) throws CameraAccessException {
        // Check if the app has permission to access the camera. If you encounter an error here, make sure to import android.Manifest in your activity. 
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
                != PackageManager.PERMISSION_GRANTED) {
            // If not, return and do not proceed with opening the camera
            return;
        }

        // Set up camera outputs and transform
        setUpCameraOutputs(width, height);
        configureTransform(width, height);

        // Get an instance of the CameraManager
        CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
        try {
            // Use a lock to ensure proper opening and closing of the camera
            if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
                throw new RuntimeException("Time out waiting to lock camera opening.");
            }
            // Open the camera with the given camera ID and set callbacks to handle camera events
            manager.openCamera(cameraServiceList[openedCamera].CameraIDD, cameraStateCallback, mBackgroundHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            throw new RuntimeException("Interrupted while trying to lock camera opening.", e);
        }
    }

La méthode openCamera dans ce bloc de code est responsable de l’ouverture de la caméra en utilisant l’API Camera2. Elle prend en compte la largeur et la hauteur de la surface d’aperçu de la caméra et configure les sorties et la transformation de la caméra (pour s’assurer que l’aperçu l’affiche correctement sans déformations). Elle utilise également un verrou pour s’assurer que la caméra est ouverte et fermée correctement. Si l’application n’a pas l’autorisation nécessaire pour accéder à la caméra, la méthode renvoie et ne procède pas à l’ouverture de la caméra.

Lorsque la méthode est lancée, elle exécute d’abord deux opérations :

2.3. SetUpCameraOutputs:
    private void setUpCameraOutputs(int width, int height) throws CameraAccessException {
        Activity activity = this;
        CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
        // Get the list of available cameras:
        cameraServiceList= new CameraService[manager.getCameraIdList().length];
        mTextureView.setGetZoomCaracteristics(new AutoFitTextureView.getZoomCaracteristics() {
            @Override
            public Rect giveRectZoom() throws CameraAccessException {
                CameraManager manager12 = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
                CameraCharacteristics characteristics = manager12.getCameraCharacteristics(cameraServiceList[openedCamera].CameraIDD);
                return characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
            }

            @Override
            public float giveMaxZoom() throws CameraAccessException {
                CameraManager manager12 = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
                CameraCharacteristics characteristics = manager12.getCameraCharacteristics(cameraServiceList[openedCamera].CameraIDD);
                return characteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)*10 ;
            }

            @Override
            public void previewRequestINT(Rect rect) {
                mPreviewRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, rect);
            }

            @Override
            public void captureSession() throws CameraAccessException {
                cameraServiceList[openedCamera].captureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null,
                        mBackgroundHandler);
            }
        });
        // Retrieve information about each camera:
        try {
            for (String cameraId : manager.getCameraIdList()) {
                boolean poscam=true; // true if camera is front-facing, false otherwise
                // Retrieve the characteristics of the camera
                CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);

                Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
                if (facing != null && facing == CameraCharacteristics.LENS_FACING_FRONT) {
                    poscam=true;
                    // poscam is true if it looks towards the face :)
                }
                else if (facing ==  CameraCharacteristics.LENS_FACING_BACK)
                {
                    poscam=false;
                    // poscam is false if we use the back camera
                }

                StreamConfigurationMap map = characteristics.get(
                        CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
                if (map == null) {
                     /*The if (map == null) condition checks if the
                     CameraCharacteristics for the current camera ID is null. If it is null,
                     the loop continues to the next camera ID, thereby skipping any further
                     processing for that particular camera.*/
                    continue;
                }

                // this allows as to see the largest dimensions we can get
                Size largest = Collections.max(
                        Arrays.asList(map.getOutputSizes(ImageFormat.JPEG)),
                        new CompareSizesByArea());

// Find out if we need to swap dimension to get the preview size relative to sensor coordinate.
                int displayRotation = activity.getDisplay().getRotation();

                mSensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);

                boolean swappedDimensions = false;
                switch (displayRotation) {
                    case Surface.ROTATION_0:
                    case Surface.ROTATION_180:
                        if (mSensorOrientation == 90 || mSensorOrientation == 270) {
                            swappedDimensions = true;
                        }
                        break;
                    case Surface.ROTATION_90:
                    case Surface.ROTATION_270:
                        if (mSensorOrientation == 0 || mSensorOrientation == 180) {
                            swappedDimensions = true;
                        }
                        break;
                    default:
                }

                // Get the dimensions of the application window using WindowMetrics
                WindowMetrics windowMetrics = activity.getWindowManager().getCurrentWindowMetrics();
                Rect bounds = windowMetrics.getBounds();
                int display_X = bounds.width();
                int display_Y = bounds.height();
                // Initialize variables for preview size
                int rotatedPreviewWidth = width;
                int rotatedPreviewHeight = height;
                int maxPreviewWidth = display_X;
                int maxPreviewHeight = display_Y;

                // Swap dimensions if needed
                if (swappedDimensions) {
                    rotatedPreviewWidth = height;
                    rotatedPreviewHeight = width;
                    maxPreviewWidth = display_X;
                    maxPreviewHeight = display_Y;
                }

                // Limit the max preview width and height
                if (maxPreviewWidth > MAX_PREVIEW_WIDTH) {
                    maxPreviewWidth = MAX_PREVIEW_WIDTH;
                }

                if (maxPreviewHeight > MAX_PREVIEW_HEIGHT) {
                    maxPreviewHeight = MAX_PREVIEW_HEIGHT;
                }

                // Choose the optimal preview size based on the available sizes and the desired dimensions.
                mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class),
                        rotatedPreviewWidth, rotatedPreviewHeight, maxPreviewWidth,
                        maxPreviewHeight, largest);

                // We fit the aspect ratio of TextureView to the size of preview we picked.
                int orientation = getResources().getConfiguration().orientation;
                if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
                    mTextureView.setAspectRatio(
                            mPreviewSize.getWidth(), mPreviewSize.getHeight());
                } else {
                    mTextureView.setAspectRatio(
                            mPreviewSize.getHeight(), mPreviewSize.getWidth());
                }

                // Check if the flash is supported.
                Boolean available = characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
                mFlashSupported = available == null ? false : available;

                // Initialize the CameraService for the current camera.
                cameraServiceList[Integer.parseInt(cameraId)]=new CameraService(cameraId,poscam);
                cameraServiceList[Integer.parseInt(cameraId)].setSensorOrientation(mSensorOrientation);
                cameraServiceList[Integer.parseInt(cameraId)].setFlashSupported(mFlashSupported);


            }
        } catch (CameraAccessException e) {
            e.printStackTrace();
        } catch (NullPointerException e) {

        }
    }

La méthode prend en compte la largeur et la hauteur de l’aperçu de la caméra en tant que paramètres et lance une CameraAccessException.

Nous commençons par initialiser le gestionnaire de caméras, obtenir la liste des caméras disponibles et configurer la TextureView personnalisée (AutoFitTextureView). Ensuite, la méthode parcourt chaque caméra disponible, récupère ses caractéristiques et configure un lecteur d’images pour les captures d’images fixes.

Ensuite, elle détermine la rotation de l’affichage et si les dimensions de l’aperçu doivent être échangées pour correspondre aux coordonnées du capteur. Elle choisit la taille d’aperçu optimale en fonction des tailles disponibles et des dimensions de l’affichage. Elle vérifie également si le flash de la caméra est pris en charge et configure un objet CameraService pour stocker des informations sur la caméra.

La classe CameraService est définie ci-dessous et nous permet de stocker des informations sur chaque caméra :

    public class CameraService{
        String CameraIDD;  // A unique identifier for the camera
        CameraDevice mCameraDevice; // A reference to the CameraDevice object
        boolean frontOrBackCamera; // A boolean to indicate if the camera is front-facing or back-facing
        boolean flashSupported; // A boolean to indicate if the camera has a flash
        CameraCaptureSession captureSession; // A reference to the CameraCaptureSession object
        int sensorOrientation; // An integer to indicate the orientation of the camera sensor

        // Getter and setter methods for the member variables
        public int getSensorOrientation() {
            return sensorOrientation;
        }

        public void setSensorOrientation(int sensorOrientation) {
            this.sensorOrientation = sensorOrientation;
        }

        public String getCameraIDD() {
            return CameraIDD;
        }

        public void setCameraIDD(String cameraIDD) {
            CameraIDD = cameraIDD;
        }

        public CameraDevice getmCameraDevice() {
            return mCameraDevice;
        }

        public void setmCameraDevice(CameraDevice mCameraDevice) {
            this.mCameraDevice = mCameraDevice;
        }

        public boolean isFrontOrBackCamera() {
            return frontOrBackCamera;
        }

        public void setFrontOrBackCamera(boolean frontOrBackCamera) {
            this.frontOrBackCamera = frontOrBackCamera;
        }

        public boolean isFlashSupported() {
            return flashSupported;
        }

        public void setCaptureSession(CameraCaptureSession captureSession) {
            this.captureSession = captureSession;
        }

        public CameraCaptureSession getCaptureSession() {
            return captureSession;
        }

        // Constructor to initialize the member variables
        public CameraService(String cameraIDD, boolean frontOrBackCamera) {
            this.CameraIDD = cameraIDD;
            this.frontOrBackCamera = frontOrBackCamera;
        }

        public void setFlashSupported(boolean flashSupported) {
            this.flashSupported = flashSupported;
        }
    }

Le but de cette classe est de fournir une encapsulation autour du périphérique de la caméra et de ses objets et propriétés associés. Elle contient des variables membres pour stocker des informations telles que l’identifiant de la caméra, l’objet CameraDevice, si la caméra est orientée vers l’avant ou vers l’arrière, si elle dispose d’un flash, et l’objet CameraCaptureSession. Elle fournit également des méthodes getter et setter pour ces variables membres. Cette classe peut être utilisée pour gérer le périphérique de la caméra et ses propriétés.

Enfin, la méthode configure un objet getZoomCaracteristics pour effectuer un zoom sur l’aperçu de la caméra et configure la session de capture de la caméra.

La méthode s’appuie également sur deux autres méthodes, chooseOptimalSize et CompareSizesByArea :

  private static Size chooseOptimalSize(Size[] choices, int textureViewWidth, int textureViewHeight, int maxWidth, int maxHeight, Size aspectRatio) {

        // Collect the supported resolutions that are at least as big as the preview Surface
        List<Size> bigEnough = new ArrayList<>();
        // Collect the supported resolutions that are smaller than the preview Surface
        List<Size> notBigEnough = new ArrayList<>();
        int w = aspectRatio.getWidth();
        int h = aspectRatio.getHeight();
        for (Size option : choices) {
            if (option.getWidth() <= maxWidth && option.getHeight() <= maxHeight &&
                    option.getHeight() == option.getWidth() * h / w) {
                if (option.getWidth() >= textureViewWidth &&
                        option.getHeight() >= textureViewHeight) {
                    bigEnough.add(option);
                } else {
                    notBigEnough.add(option);
                }
            }
        }

        // Pick the smallest of those big enough. If there is no one big enough, pick the
        // largest of those not big enough.
        if (bigEnough.size() > 0) {
            return Collections.min(bigEnough, new CompareSizesByArea());
        } else if (notBigEnough.size() > 0) {
            return Collections.max(notBigEnough, new CompareSizesByArea());
        } else {
            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());
        }

    }

Maintenant, revenons à la méthode openCamera et concentrons-nous sur la méthode suivante :

2.4. configureTransform
    private void configureTransform(int viewWidth, int viewHeight) {
        Activity activity = this;

        // Check if TextureView, preview size or activity are null, then return
        if (null == mTextureView || null == mPreviewSize || null == activity) {
            return;
        }

        // Get the current rotation of the display
        int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();

        // Create a new Matrix object
        Matrix matrix = new Matrix();

        // Create a rectangle representing the view bounds
        RectF viewRect = new RectF(0, 0, viewWidth, viewHeight);

        // Create a rectangle representing the preview size
        RectF bufferRect = new RectF(0, 0, mPreviewSize.getHeight(), mPreviewSize.getWidth());

        // Calculate the center point of the view bounds
        float centerX = viewRect.centerX();
        float centerY = viewRect.centerY();

        // If the rotation is 90 or 270 degrees, adjust the buffer rectangle
        if (Surface.ROTATION_90 == rotation || Surface.ROTATION_270 == rotation) {
            bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY());

            // Scale the preview to fill the view, then rotate it
            matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL);
            float scale = Math.max(
                    (float) viewHeight / mPreviewSize.getHeight(),
                    (float) viewWidth / mPreviewSize.getWidth());
            matrix.postScale(scale, scale, centerX, centerY);
            matrix.postRotate(90 * (rotation - 2), centerX, centerY);

            // Store the rotation angle in a variable
            int rot= 90 * (rotation - 2);
        }
        // If the rotation is 180 degrees, just rotate the preview
        else if (Surface.ROTATION_180 == rotation) {
            matrix.postRotate(180, centerX, centerY);
        }

        // Apply the matrix to the TextureView
        mTextureView.setTransform(matrix);
    }

La méthode configureTransform() est responsable de la configuration de la matrice de transformation pour la TextureView, qui affiche l’aperçu de la caméra. La méthode prend en compte les dimensions de la TextureView en tant que paramètres et vérifie que tous les composants nécessaires (mTextureView, mPreviewSize et activity) ne sont pas nuls avant de procéder.

Ensuite, la méthode obtient la rotation actuelle de l’appareil à partir du WindowManager et initialise un nouvel objet Matrix. Elle crée également deux objets RectF pour représenter les zones de vue et d’aperçu, respectivement, et calcule le point central de la vue.

Si l’appareil est tourné de 90 ou 270 degrés, la méthode calcule le facteur d’échelle requis pour remplir la vue avec l’aperçu et configure en conséquence la matrice de transformation. La matrice est d’abord configurée pour remplir l’intégralité de la vue, puis mise à l’échelle par le facteur approprié, et enfin tournée d’un angle en fonction de la rotation actuelle de l’appareil. La transformation résultante est ensuite appliquée à la TextureView.

Si l’appareil est tourné de 180 degrés, la méthode fait simplement pivoter la matrice de 180 degrés et l’applique à la TextureView.

Revenons une fois de plus à la méthode openCamera et concentrons-nous sur la deuxième partie du code (pas besoin de le copier et de le coller à nouveau si tu l’as déjà) :

  // Get an instance of the CameraManager
    CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
    try {
        // Use a lock to ensure proper opening and closing of the camera
        if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
            throw new RuntimeException("Time out waiting to lock camera opening.");
        }
        // Open the camera with the given camera ID and set callbacks to handle camera events
        manager.openCamera(cameraServiceList[openedCamera].CameraIDD, mStateCallback, mBackgroundHandler);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    } catch (InterruptedException e) {
        throw new RuntimeException("Interrupted while trying to lock camera opening.", e);
    }
}

À cette étape, si aucune exception ou erreur n’a été levée, cameraServiceList devrait contenir une liste des caméras disponibles et de leurs caractéristiques.

Ensuite, le cameraManager tentera d’exécuter openCamera en utilisant cameraID en tant qu’entrée, ainsi que cameraStateCallback et mBackgroundHandler, que nous définirons maintenant.

2.5. cameraStateCallback
    private final CameraDevice.StateCallback cameraStateCallback = new CameraDevice.StateCallback() {

        @Override
        public void onOpened(@NonNull CameraDevice cameraDevice) {
            // This method is called when the camera is opened. We release the lock and set the camera device
            // for the current camera service, then create a camera preview session.
            mCameraOpenCloseLock.release();
            cameraServiceList[openedCamera].mCameraDevice = cameraDevice;
            createCameraPreviewSession();
        }

        @Override
        public void onDisconnected(@NonNull CameraDevice cameraDevice) {
            // This method is called when the camera is disconnected. We release the lock, close the camera device,
            // and set the camera device for the current camera service to null.
            mCameraOpenCloseLock.release();
            cameraDevice.close();
            cameraServiceList[openedCamera].mCameraDevice  = null;
        }

        @Override
        public void onError(@NonNull CameraDevice cameraDevice, int error) {
            // This method is called when an error occurs with the camera device. We release the lock, close the camera device,
            // set the camera device for the current camera service to null, and finish the activity.
            mCameraOpenCloseLock.release();
            cameraDevice.close();
            cameraServiceList[openedCamera].mCameraDevice  = null;
            finish();

        }
    };

Ce code définit une implémentation de l’interface CameraDevice.StateCallback en tant que variable privée finale nommée cameraStateCallback. Cette interface fournit des méthodes qui sont appelées lorsque l’état du périphérique de la caméra change.

La méthode onOpened() est appelée lorsque le périphérique de la caméra est ouvert, et la méthode libère un verrou qui était précédemment détenu par mCameraOpenCloseLock. Ensuite, elle définit l’objet CameraDevice dans la variable mCameraDevice correspondante de l’objet CameraService et appelle createCameraPreviewSession().

    private void createCameraPreviewSession() {
        try {

            SurfaceTexture texture = mTextureView.getSurfaceTexture();
            assert texture != null;
            // We configure the size of default buffer to be the size of camera preview we want.
            texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());

            // This is the output Surface we need to start preview.
            Surface surface = new Surface(texture);

            // We set up a CaptureRequest.Builder with the output Surface.
            mPreviewRequestBuilder
                    = cameraServiceList[openedCamera].mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
            mPreviewRequestBuilder.addTarget(surface);

            // Here, we create a CameraCaptureSession for camera preview.

            cameraServiceList[openedCamera].mCameraDevice.createCaptureSession(Arrays.asList(surface),
                    new CameraCaptureSession.StateCallback() {
                /* the createCaptureSession is deprecated, as of writing this tutorial, the code provided still works.
                  The new way of implementing this method is through createCaptureSession(SessionConfiguration).*/

                        @Override
                        public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
                            // The camera is already closed

                            if (null == cameraServiceList[openedCamera].mCameraDevice) {

                                return;
                            }

                            // When the session is ready, we start displaying the preview.
                            cameraServiceList[openedCamera].captureSession=cameraCaptureSession;
                            try {
                                // Auto focus should be continuous for camera preview.
                                mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
                                        CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);


                                // Finally, we start displaying the camera preview.
                                mPreviewRequest = mPreviewRequestBuilder.build();
                                cameraServiceList[openedCamera].captureSession.setRepeatingRequest(mPreviewRequest,null, mBackgroundHandler);
                            } catch (CameraAccessException e) {
                                e.printStackTrace();
                            }
                            updatePreview();
                        }

                        @Override
                        public void onConfigureFailed(
                                @NonNull CameraCaptureSession cameraCaptureSession) {
                        }
                    }, null
            );
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

La méthode commence par obtenir l’objet SurfaceTexture de notre AutofitTextureView et définir sa taille de tampon par défaut sur la taille de l’aperçu de la caméra. Un objet Surface est ensuite créé en utilisant l’objet SurfaceTexture.

Ensuite, un objet CaptureRequest.Builder est créé pour le périphérique de la caméra avec un modèle TEMPLATE_RECORD, et l’objet Surface est ajouté en tant que cible.

Une CameraCaptureSession est ensuite créée en utilisant le CameraDevice et l’objet Surface. La méthode onConfigured() de l’interface StateCallback est implémentée pour gérer quand la session est prête. Si le périphérique de la caméra est fermé, la méthode retourne une exception. Sinon, l’aperçu est démarré en définissant le mode de mise au point, en construisant et en répétant la CaptureRequest en utilisant la CameraCaptureSession. Cela lancera la méthode updatePreview() que nous définirons ci-dessous.

    private void updatePreview() {
        if (null == cameraServiceList[openedCamera].mCameraDevice) {
            return;
        }
        try {
            setUpCaptureRequestBuilder(mPreviewRequestBuilder);
            cameraServiceList[openedCamera].captureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, mBackgroundHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

setUpCaptureRequestBuilder(mPreviewRequestBuilder) est défini en dessous:

private void setUpCaptureRequestBuilder(CaptureRequest.Builder builder) {
builder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);
}

Revenons à cameraStateCallback :

La méthode onDisconnected() est appelée lorsque le périphérique de la caméra est déconnecté. Elle libère le verrou qui était détenu par mCameraOpenCloseLock, ferme le périphérique de la caméra et définit la variable mCameraDevice correspondante de l’objet CameraService à null.

La méthode onError() est appelée lorsqu’une erreur se produit dans le périphérique de la caméra. Elle libère le verrou qui était détenu par mCameraOpenCloseLock, ferme le périphérique de la caméra, définit la variable mCameraDevice correspondante de l’objet CameraService à null et termine l’activité.

2.6. mBackgroundHandler:
private void startBackgroundThread() {
    mBackgroundThread = new HandlerThread("CameraBackground");
    mBackgroundThread.start();
    /* this is to allow the camera operations to run on a separate thread and avoid blocking the UI*/
    mBackgroundHandler = new Handler(mBackgroundThread.getLooper());
    /* this is to communicate with the thread*/
}
private void stopBackgroundThread() {
mBackgroundThread.quitSafely();
try {
mBackgroundThread.join();
mBackgroundThread = null;
mBackgroundHandler = null;
} catch (InterruptedException e) {
e.printStackTrace();
}
}

Réflexions finales :

L’intégralité du code de ce tutoriel est disponible sur GitHub à l’adresse suivante :

https://github.com/lizardanddog/tutoForCamera

Nous avons également inclus dans le code une méthode pour fermer la caméra (définie ci-dessous) :

    private void closeCamera() {
        try {
            mCameraOpenCloseLock.acquire();
            if (null != cameraServiceList[openedCamera].captureSession) {
                cameraServiceList[openedCamera].captureSession.close();
                cameraServiceList[openedCamera].captureSession = null;
            }
            if (null != cameraServiceList[openedCamera].mCameraDevice) {
                cameraServiceList[openedCamera].mCameraDevice.close();
                cameraServiceList[openedCamera].mCameraDevice = null;
            }
            if (null != mImageReader) {
                mImageReader.close();
                mImageReader = null;
            }
        } catch (InterruptedException e) {
            throw new RuntimeException("Interrupted while trying to lock camera closing.", e);
        } finally {
            mCameraOpenCloseLock.release();
        }
    }

Dans la prochaine partie, nous nous concentrerons sur la façon de capturer des images et des vidéos et de les enregistrer sur le téléphone.

About us:

https://www.cloco.ai

https://www.lizardanddog.com

Leave a comment