Приложение Android Camera2 ломается после выхода из режима камеры и повторного входа

Я пытаюсь реализовать простое приложение для фотосъемки с камеры на основе руководства, расположенного здесь: https://android.jlelse.eu/наименее-вы-можете-с-камерой2-api-2971c8c81b8b

Приложение отлично работает с первого раза. Предварительный просмотр показывает, и я могу делать снимки, которые сохраняются правильно.

Моя проблема заключается в том, что когда я закрываю приложение, наношу ответный удар, сворачиваю приложение и т. д., а затем возвращаюсь к работе с камерой, предварительный просмотр больше ничего не показывает, и приложение вылетает, когда я пытаюсь сделать снимок.

Я почти уверен, что каким-то образом неправильно отключаю камеру, но я просмотрел свой код построчно по сравнению с работающей реализацией, и я не вижу, что я делаю по-другому/неправильно. .

package com.example.cameratest;

import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.os.Environment;
import android.os.Handler;
import android.os.HandlerThread;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Size;
import android.view.Display;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Toast;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.Locale;

public class SecondCamera extends AppCompatActivity {

    // Define the variables we need to use the camera

    // Our request code can be anything. I like 8.
    private static final int CAMERA_REQUEST_CODE = 8;
    private CameraManager cameraManager;
    private int cameraFacing;
    private TextureView.SurfaceTextureListener surfaceTextureListener;
    private String cameraId;
    private Size previewSize;
    private CameraDevice cameraDevice;
    private TextureView textureView;
    private CameraCaptureSession cameraCaptureSession;

    private HandlerThread backgroundThread;
    private Handler backgroundHandler;

    private CameraDevice.StateCallback stateCallback;

    private CaptureRequest.Builder captureRequestBuilder;
    private CaptureRequest captureRequest;
    private File galleryFolder;
    private WindowManager windowManager;
    private int windowHeight;
    private int windowWidth;

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

        // Get the size of our display in order to properly scale the camera view
        Display display = getWindowManager().getDefaultDisplay();
        Point size = new Point();
        display.getSize(size);
        windowHeight = size.y;
        windowWidth = size.x;

        textureView = (TextureView) findViewById(R.id.cameraTextureView);

        // Let's ask for permission to use the camera feature
        ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}, CAMERA_REQUEST_CODE);
        // Get the camera system service
        cameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
        // Make sure we use the back camera
        cameraFacing = CameraCharacteristics.LENS_FACING_BACK;

        // Set up a listener to communicate to our TextureView
        surfaceTextureListener = new TextureView.SurfaceTextureListener(){
            @Override
            public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
                setUpCamera();
                openCamera();
            }

            @Override
            public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) {

            }

            @Override
            public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
                return false;
            }

            @Override
            public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {

            }
        };

        textureView.setSurfaceTextureListener(surfaceTextureListener);

        // Manage the three states of our CameraDevice. Opened, Closed, and Error
        stateCallback = new CameraDevice.StateCallback(){

            @Override
            public void onOpened(@NonNull CameraDevice cameraDevice) {
                // Grab our camera device and start up the preview
                SecondCamera.this.cameraDevice = cameraDevice;
                createPreviewSession();
                createImageGallery();
            }

            @Override
            public void onDisconnected(@NonNull CameraDevice cameraDevice) {
                // Close and disconnect the cameraDevice
                cameraDevice.close();
                SecondCamera.this.cameraDevice = null;
            }

            @Override
            public void onError(@NonNull CameraDevice cameraDevice, int i) {
                // Close and disconnect the cameraDevice
                cameraDevice.close();
                SecondCamera.this.cameraDevice = null;
            }
        };
    }

    @Override
    protected void onResume(){
        super.onResume();
        openBackgroundThread();
        // If our texture view is available, let's set up and open the camera on it.
        if (textureView.isAvailable()){
            setUpCamera();
            openCamera();
        } else {
            // If not, we need to set up the SurfaceTextureListener,
            // which will do the same once the texture view is available
            textureView.setSurfaceTextureListener(surfaceTextureListener);
        }
    }

    @Override
    protected void onPause(){
        // Close the Camera and Background Thread to avoid memory leakage
        closeBackgroundThread();
        closeCamera();
        super.onPause();
    }

    /**
     * Close the CameraCaptureSession and CameraDevice
     */
    private void closeCamera(){
        if (cameraCaptureSession != null){
            cameraCaptureSession.close();
            cameraCaptureSession = null;
        }
        if (cameraDevice != null){
            cameraDevice.close();
            cameraDevice = null;
        }
    }

    /**
     * Shut down our backgroundThread and handler
     */
    private void closeBackgroundThread(){
        backgroundThread.quitSafely();
        try {
            backgroundThread.join();
            backgroundThread = null;
            backgroundHandler = null;
        } catch (InterruptedException ie){
            ie.printStackTrace();
        }
    }

    /**
     * Open a thread in the background in order to run the camera
     */
    private void openBackgroundThread(){
        backgroundThread = new HandlerThread("Camera Background Thread");
        backgroundThread.start();
        backgroundHandler = new Handler(backgroundThread.getLooper());
    }

    /**
     * Establish the camera to use, and get information to scale our preview correctly
     */
    private void setUpCamera(){
        try{
            for (String cameraID: cameraManager.getCameraIdList()){
                CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraID);
                // If we find the appropriate camera:
                if (characteristics.get(CameraCharacteristics.LENS_FACING) == cameraFacing){
                    // Get the preview size we need, and set this class's camera ID
                    StreamConfigurationMap streamConfigurationMap = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
                    // The first element in the list of output sizes is the highest resolution one.
                    // Get a Size object which is prepared for a SurfaceTexture.
                    Size[] possibleSizes = streamConfigurationMap.getOutputSizes(SurfaceTexture.class);
                    previewSize = chooseOptimalSize(possibleSizes, windowWidth, windowHeight);
                    this.cameraId = cameraID;
                }
            }
            if (this.cameraId == null){
                Toast.makeText(this, "ERROR, NO CAMERA FOUND", Toast.LENGTH_SHORT).show();
            }
        } catch (CameraAccessException cae){
            cae.printStackTrace();
        }
    }

    /**
     * Attempt to open the camera in the background thread
     * if we find that we have been granted permission.
     */
    private void openCamera(){
        try {
            if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED){
                cameraManager.openCamera(cameraId, stateCallback, backgroundHandler);
            }
            else {
                Toast.makeText(this, "Camera Permissions must be enabled for this activity to function", Toast.LENGTH_SHORT).show();
            }
        } catch (CameraAccessException cae){
            cae.printStackTrace();
        }
    }

    /**
     * Initialize a preview for our camera screen, so the user can
     * see in real time what the camera sees.
     */
    private void createPreviewSession(){
        try {
            // Get the surfaceTexture out of our textureView. This is what we stream to
            SurfaceTexture surfaceTexture = textureView.getSurfaceTexture();
            // We want to set our size correctly based on the preview Size from before
            surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight());
            // Set up a Surface based on our Surface Texture
            Surface previewSurface = new Surface(surfaceTexture);
            // We want to build a request for a preview style stream, and send it to our surface
            captureRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            captureRequestBuilder.addTarget(previewSurface);

            cameraDevice.createCaptureSession(Collections.singletonList(previewSurface), new CameraCaptureSession.StateCallback() {
                @Override
                public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
                    if (cameraDevice != null){
                        try {
                            captureRequest = captureRequestBuilder.build();
                            SecondCamera.this.cameraCaptureSession = cameraCaptureSession;
                            SecondCamera.this.cameraCaptureSession.setRepeatingRequest(captureRequest, null, backgroundHandler);
                        } catch (CameraAccessException cae) {
                            cae.printStackTrace();
                        }
                    }
                }

                @Override
                public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
                    // Do nothing?
                }

            }, backgroundHandler);
        } catch (CameraAccessException cae){
            cae.printStackTrace();
        }
    }

    /**
     * Initialize a folder in our pictures library to save photos to
     */
    private void createImageGallery(){
        File storageDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
        galleryFolder = new File(storageDirectory, getResources().getString(R.string.app_name));
        if (!galleryFolder.exists()){
            boolean wasCreated = galleryFolder.mkdirs();
            if (!wasCreated){
                System.out.println("Failed to create directory " + galleryFolder.getPath());
            }
        }
    }

    /**
     * Create a temp file to store our image to
     */
    private File createImageFile(File galleryFolder) throws IOException {
        // Grab our timestamp and use it to create a unique image name
        String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmsss", Locale.getDefault()).format(new Date());
        String imageFilename = getResources().getString(R.string.app_name) + timeStamp;
        Toast.makeText(this, "Created image " + imageFilename + ".jpg", Toast.LENGTH_SHORT).show();
        return File.createTempFile(imageFilename, ".jpg", galleryFolder);

    }

    /**
     * Capture everything on the screen, and output it to our file
     */
    public void takePhoto(View view){
        try (FileOutputStream outputPhoto = new FileOutputStream(createImageFile(galleryFolder))) {
            lock();
            textureView.getBitmap().compress(Bitmap.CompressFormat.PNG, 100, outputPhoto);
        } catch (IOException ioe) {
            ioe.printStackTrace();
        } finally {
            unlock();
        }
    }

    /**
     * Lock our camera preview, as if the shutter of a camera
     */
    private void lock(){
        try {
            // Lock the screen for a second
            cameraCaptureSession.capture(captureRequestBuilder.build(), null, backgroundHandler);
        } catch (CameraAccessException cae){
            cae.printStackTrace();
        }
    }

    /**
     * Unlock our camera preview, allowing the user to see freely again
     */
    private void unlock(){
        try {
            // Go back to the repeating preview request
            cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(),
                    null, backgroundHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

    /**
     * Do some calculations to figure out how to show the correct
     * aspect ratio for our camera preview
     *
     * @param outputSizes The array of possible output sizes
     * @param width The width of our device screen
     * @param height The height of our device screen
     * @return The size to set our display to
     */
    private Size chooseOptimalSize(Size[] outputSizes, int width, int height) {
        double preferredRatio = height / (double) width;
        Size currentOptimalSize = outputSizes[0];
        double currentOptimalRatio = currentOptimalSize.getWidth() / (double) currentOptimalSize.getHeight();
        for (Size currentSize : outputSizes) {
            double currentRatio = currentSize.getWidth() / (double) currentSize.getHeight();
            if (Math.abs(preferredRatio - currentRatio) <
                    Math.abs(preferredRatio - currentOptimalRatio)) {
                currentOptimalSize = currentSize;
                currentOptimalRatio = currentRatio;
            }
        }
        return currentOptimalSize;
    }

}

В логах при сбое вижу:

E/CameraDeviceState: Cannot call configure while in state: 0
E/AndroidRuntime: FATAL EXCEPTION: Camera Background Thread
    Process: com.example.cameratest, PID: 21313
    java.lang.IllegalStateException: Session has been closed; further changes are illegal.
        at android.hardware.camera2.impl.CameraCaptureSessionImpl.checkNotClosed(CameraCaptureSessionImpl.java:627)
        at android.hardware.camera2.impl.CameraCaptureSessionImpl.setRepeatingRequest(CameraCaptureSessionImpl.java:234)
        at com.example.cameratest.SecondCamera$3.onConfigured(SecondCamera.java:263)
        at java.lang.reflect.Method.invoke(Native Method)
        at android.hardware.camera2.dispatch.InvokeDispatcher.dispatch(InvokeDispatcher.java:39)
        at android.hardware.camera2.dispatch.HandlerDispatcher$1.run(HandlerDispatcher.java:65)
        at android.os.Handler.handleCallback(Handler.java:751)
        at android.os.Handler.dispatchMessage(Handler.java:95)
        at android.os.Looper.loop(Looper.java:154)
        at android.os.HandlerThread.run(HandlerThread.java:61)

Это заставляет меня поверить, что с моим CameraCaputreSession что-то не так, но я понятия не имею, что именно.

РЕДАКТИРОВАТЬ

Изменение линии

cameraManager.openCamera(cameraId, stateCallback, backgroundHandler);

В моем методе openCamera() для передачи null в качестве третьего параметра вместо моего backgroundHandler, кажется, это исправлено, но, честно говоря, я совершенно не понимаю, почему. Может ли кто-нибудь объяснить это мне? Было ли это правильным решением?

3
0
4 097
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Один из вариантов — вы запускаете свой фоновый поток в onResume(), но вы также пытаетесь открыть камеру в OnSurfaceTextureAvailable в TextureViewListener.

При первом запуске вы подключаете TextureViewListener только после настройки фонового потока в onResume, но никогда не отменяете регистрацию этого слушателя. При втором запуске, возможно, обратный вызов onSurfaceTextureAvailable выполняется до onResume(), потому что пользовательский интерфейс приложения становится видимым в onStart(), а не в onResume().

Это откроет камеру с нулевым фоновым потоком (переходя к основному потоку), а затем onResume сработает и откроет камеру во второй раз, вытеснив первый объект камеры из onSurfaceTextureAvailable. В результате возникают ошибки при выполнении дальнейших операций с этим первым объектом камеры.

Одним из решений может быть просто отмена регистрации TextureViewListener в onPaused или реструктуризация вещей таким образом, чтобы получение раннего обратного вызова onSurfaceTextureAvailable также устанавливало фоновый поток.

Я не могу отблагодарить вас за ваш ответ, это была именно проблема! Я отметил это как принятый ответ. Очень ценю советы, большое спасибо!

MG_LM_SE 13.06.2019 16:03

Другие вопросы по теме