Pull to refresh
0
Badoo
Big Dating

Побеждаем Android Camera2 API с помощью RxJava2 (часть 1)

Reading time 21 min
Views 37K


Как известно, RxJava идеально подходит для решения двух задач: обработки потоков событий и работы с асинхронными методами. В одном из предыдущих постов я показал, как можно построить цепочку операторов, обрабатывающую поток событий от сенсора. А сегодня я хочу продемонстрировать, как RxJava применяется для работы с существенно асинхронным API. В качестве такого API я выбрал Camera2 API.


Ниже будет показан пример использования Camera2 API, который пока довольно слабо задокументирован и изучен сообществом. Для его укрощения будет использована RxJava2. Вторая версия этой популярной библиотеки вышла сравнительно недавно, и примеров на ней тоже немного.


Для кого этот пост? Я рассчитываю, что читатель – умудрённый опытом, но всё ещё любознательный Android-разработчик. Очень желательны базовые знания о реактивном программировании (хорошее введение – здесь) и понимание Marble Diagrams. Пост будет полезен тем, кто хочет проникнуться реактивным подходом, а также тем, кто хочет использовать Camera2 API в своих проектах. Предупреждаю, будет много кода!


Исходники проекта можно найти на GitHub.


Подготовка проекта


Добавим сторонние зависимости в наш проект.


Retrolambda


При работе с RxJava совершенно необходима поддержка лямбд – иначе код будет выглядеть просто ужасно. Так что если вы ещё не перешли на Android Studio 3.0, добавим Retrolambda в наш проект.


buildscript {
     dependencies {
      classpath 'me.tatarka:gradle-retrolambda:3.6.0'
   }
}

apply plugin: 'me.tatarka.retrolambda'

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


android {
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

Полные инструкции.


RxJava2


compile 'io.reactivex.rxjava2:rxjava:2.1.0'

Актуальную версию, полные инструкции и документацию ищите тут.


RxAndroid


Полезная библиотека при использовании RxJava на Android. В основном используется ради AndroidSchedulers. Репозиторий.


compile 'io.reactivex.rxjava2:rxandroid:2.0.1'

Camera2 API


В своё время я участвовал в code review модуля, написанного с использованием Camera1 API, и был неприятно удивлён неизбежными в силу дизайна API concurrency issues. Видимо, в Google тоже осознали проблему и задепрекейтили первую версию API. Взамен предлагается использовать Camera2 API. Вторая версия доступна на Android Lollipop и новее.


Давайте же на него посмотрим.


Первые впечатления


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


Референсная имплементация


Google предлагает пример приложения Camera2Basic.


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


Шаги для получения снимка


Если кратко, то последовательность действий для получения снимка такова:


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

Выбор устройства


Прежде всего нам понадобится CameraManager.


mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);

Этот класс позволяет получать информацию о существующих в системе камерах и подключаться к ним. Камер может быть несколько, в смартфонах обычно их две: фронтальная и тыловая.


Получаем список камер.


String[] cameraIdList = mCameraManager.getCameraIdList();

Вот так сурово – просто список строковых айдишников.


Теперь получим список характеристик для каждой камеры.


for (String cameraId : cameraIdList) {
    CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(cameraId);
    ...
}

CameraCharacteristics содержит огромное количество ключей, по которым можно получать информацию о камере.


Чаще всего на этапе выбора камеры смотрят на то, куда направлена камера. Для этого необходимо получить значение по ключу CameraCharacteristics.LENS_FACING.


Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);

Камера может быть фронтальной (CameraCharacteristics.LENS_FACING_FRONT), тыловой (CameraCharacteristics.LENS_FACING_BACK) или подключаемой (CameraCharacteristics.LENS_FACING_EXTERNAL).


Функция выбора камеры с предпочтением по ориентации может выглядеть примерно так:


@Nullable
private static String getCameraWithFacing(@NonNull CameraManager manager, int lensFacing) throws CameraAccessException {
    String possibleCandidate = null;
    String[] cameraIdList = manager.getCameraIdList();
    if (cameraIdList.length == 0) {
        return null;
    }
    for (String cameraId : cameraIdList) {
        CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);

        StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
        if (map == null) {
            continue;
        }

        Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
        if (facing != null && facing == lensFacing) {
            return cameraId;
        }

        //just in case device don't have any camera with given facing
        possibleCandidate = cameraId;
    }
    if (possibleCandidate != null) {
        return possibleCandidate;
    }
    return cameraIdList[0];
}

Отлично, теперь у нас есть id камеры нужной ориентации (или любой другой, если нужной не нашлось). Пока всё довольно просто, никаких асинхронных действий.


Создаем Observable


Мы подходим к асинхронным методам API. Каждый из них мы превратим в Observable с помощью метода create.


openCamera


Устройство перед использованием нужно открыть с помощью метода CameraManager.openCamera.


void openCamera (String cameraId, 
    CameraDevice.StateCallback callback, 
    Handler handler)

В этот метод передаём id выбранной камеры, колбек для получения асинхронного результата и Handler, если мы хотим, чтобы методы колбека вызывались в потоке этого Handler.


Вот мы и столкнулись с первым асинхронным методом. Оно и понятно, ведь инициализация устройства – длительный и дорогой процесс.


Давайте взглянем на CameraDevice.StateCallback.



В реактивном мире эти методы будут соответствовать событиям. Давайте сделаем Observable, который будет генерировать события, когда API камеры будет вызывать onOpened, onClosed, onDisconnected. Чтобы мы могли различать эти события, создадим enum:


public enum DeviceStateEvents {
        ON_OPENED,
        ON_CLOSED,
        ON_DISCONNECTED
    }

А чтобы в реактивном потоке (тут и далее я буду называть реактивным потоком последовательность реактивных операторов – не путать со Thread) была возможность что-то сделать с устройством, мы добавим в генерируемое событие ссылку на CameraDevice. Самый простой способ – генерировать Pair<DeviceStateEvents, CameraDevice>. Для создания Observable воспользуемся методом create (напомню, мы используем RxJava2, так что теперь нам совсем не стыдно это делать).


Вот сигнатура метода create:


public static <T> Observable<T> create(ObservableOnSubscribe<T> source)

То есть нам нужно передать в него объект, реализующий интерфейс ObservableOnSubscribe<T>. Этот интерфейс содержит всего один метод


void subscribe(@NonNull ObservableEmitter<T> e) throws Exception;

который вызывается каждый раз, когда Observer подписывается (subscribe) на наш Observable.


Посмотрим, что такое ObservableEmitter.


public interface ObservableEmitter<T> extends Emitter<T> {
    void setDisposable(@Nullable Disposable d);
    void setCancellable(@Nullable Cancellable c);
    boolean isDisposed();
    ObservableEmitter<T> serialize();
}

Уже хорошо. С помощью методов setDisposable/setCancellable можно задать действие, которое будет выполнено, когда от нашего Observable отпишутся. Это крайне полезно, если при создании Observable мы открывали ресурс, который надо закрывать. Мы могли бы создать Disposable, в котором закрывать устройство при unsubscribe, но мы хотим реагировать на событие onClosed, поэтому делать этого не будем.


Метод isDisposed позволяет проверять, подписан ли кто-то ещё на наш Observable.


Заметим, что ObservableEmitter расширяет интерфейс Emitter.


public interface Emitter<T> {
    void onNext(@NonNull T value);
    void onError(@NonNull Throwable error);
    void onComplete();
}

Вот эти методы нам и нужны! Мы будем вызывать onNext каждый раз, когда Camera API будет вызывать колбеки интерфейса CameraDevice.StateCallback onOpened / onClosed / onDisconnected; и мы будем вызывать onError, когда Camera API будет вызывать колбек onError.


Итак, применим наши знания. Mетод, создающий Observable, может выглядеть так (ради читабельности я убрал проверки на isDisposed(), полный код со скучными проверками смотрите на GitHub):


public static Observable<Pair<DeviceStateEvents, CameraDevice>> openCamera(
        @NonNull String cameraId,
        @NonNull CameraManager cameraManager
    ) {
        return Observable.create(observableEmitter -> {
               cameraManager.openCamera(cameraId, new CameraDevice.StateCallback() {
                @Override
                public void onOpened(@NonNull CameraDevice cameraDevice) {
                        observableEmitter.onNext(new Pair<>(DeviceStateEvents.ON_OPENED, cameraDevice));
                }

                @Override
                public void onClosed(@NonNull CameraDevice cameraDevice) {
                        observableEmitter.onNext(new Pair<>(DeviceStateEvents.ON_CLOSED, cameraDevice));
                        observableEmitter.onComplete();
                }

                @Override
                public void onDisconnected(@NonNull CameraDevice cameraDevice) {
                        observableEmitter.onNext(new Pair<>(DeviceStateEvents.ON_DISCONNECTED, cameraDevice));
                        observableEmitter.onComplete();
                }

                @Override
                public void onError(@NonNull CameraDevice camera, int error) {
                        observableEmitter.onError(new OpenCameraException(OpenCameraException.Reason.getReason(error)));
                }
            }, null);
        });
    }

Супер! Мы только что стали чуть более реактивными!


Как я уже говорил, все методы Camera2 API принимают Handler как один из параметров. Передавая null, мы будем получать вызовы колбеков в текущем потоке. В нашем случае это поток, в котором был вызван subscribe, то есть Main Thread.


createCaptureSession


Теперь, когда у нас есть CameraDevice, можно открыть CaptureSession. Не будем медлить!


Для этого воспользуемся методом CameraDevice.createCaptureSession. Вот его сигнатура:


public abstract void createCaptureSession(@NonNull List<Surface> outputs,
            @NonNull CameraCaptureSession.StateCallback callback, @Nullable Handler handler)
            throws CameraAccessException;

На вход подаётся список Surface (где его взять, разберёмся чуть позже) и CameraCaptureSession.StateCallback. Давайте посмотрим, какие в нём есть методы.



Богато! Но мы уже знаем, как побеждать колбеки. Создадим Observable, который будет генерировать события, когда Camera API будет вызывать эти методы. Чтобы их различать, создадим enum.


    public enum CaptureSessionStateEvents {
        ON_CONFIGURED,
        ON_READY,
        ON_ACTIVE,
        ON_CLOSED,
        ON_SURFACE_PREPARED
    }

А чтобы в реактивном потоке был объект CameraCaptureSession, будем генерировать не просто CaptureSessionStateEvent, а Pair<CaptureSessionStateEvents, CameraCaptureSession>. Вот как может выглядеть код метода, создающего такой Observable (проверки снова убраны для читабельности):


@NonNull
public static Observable<Pair<CaptureSessionStateEvents, CameraCaptureSession>> createCaptureSession(
    @NonNull CameraDevice cameraDevice,
    @NonNull List<Surface> surfaceList
) {
    return Observable.create(observableEmitter -> {
        cameraDevice.createCaptureSession(surfaceList, new CameraCaptureSession.StateCallback() {

            @Override
            public void onConfigured(@NonNull CameraCaptureSession session) {
                observableEmitter.onNext(new Pair<>(CaptureSessionStateEvents.ON_CONFIGURED, session));
            }

            @Override
            public void onConfigureFailed(@NonNull CameraCaptureSession session) {
                observableEmitter.onError(new CreateCaptureSessionException(session));
            }

            @Override
            public void onReady(@NonNull CameraCaptureSession session) {
                observableEmitter.onNext(new Pair<>(CaptureSessionStateEvents.ON_READY, session));
            }

            @Override
            public void onActive(@NonNull CameraCaptureSession session) {
                observableEmitter.onNext(new Pair<>(CaptureSessionStateEvents.ON_ACTIVE, session));
            }

            @Override
            public void onClosed(@NonNull CameraCaptureSession session) {
                observableEmitter.onNext(new Pair<>(CaptureSessionStateEvents.ON_CLOSED, session));
                observableEmitter.onComplete();
            }

            @Override
            public void onSurfacePrepared(@NonNull CameraCaptureSession session, @NonNull Surface surface) {
                observableEmitter.onNext(new Pair<>(CaptureSessionStateEvents.ON_SURFACE_PREPARED, session));
            }
        }, null);
    });
}

setRepeatingRequest


Для того чтобы на экране появилась живая картинка с камеры, необходимо постоянно получать новые изображения с устройства и передавать их для отображения. Для этого в API есть удобный метод CameraCaptureSession.setRepeatingRequest.


int setRepeatingRequest(@NonNull CaptureRequest request,
            @Nullable CaptureCallback listener, @Nullable Handler handler)
            throws CameraAccessException;

Применяем уже знакомый нам приём, чтобы сделать эту операцию реактивной. Смотрим на интерфейс CameraCaptureSession.CaptureCallback.



Опять же, мы хотим различать генерируемые события и для этого создадим enum.


public enum CaptureSessionEvents {
        ON_STARTED,
        ON_PROGRESSED,
        ON_COMPLETED,
        ON_SEQUENCE_COMPLETED,
        ON_SEQUENCE_ABORTED
    }

Мы видим, что в методы передаётся достаточно много информации, которую мы хотим иметь в реактивном потоке, в том числе CameraCaptureSession, CaptureRequest, CaptureResult, поэтому просто Pair<> нам уже не подойдёт – создадим POJO:


  public static class CaptureSessionData {
        final CaptureSessionEvents event;
        final CameraCaptureSession session;
        final CaptureRequest request;
        final CaptureResult result;

        CaptureSessionData(CaptureSessionEvents event, CameraCaptureSession session, CaptureRequest request, CaptureResult result) {
            this.event = event;
            this.session = session;
            this.request = request;
            this.result = result;
        }
    }

Создание CameraCaptureSession.CaptureCallback вынесем в отдельный метод.


@NonNull
private static CameraCaptureSession.CaptureCallback createCaptureCallback(final ObservableEmitter<CaptureSessionData> observableEmitter) {
    return new CameraCaptureSession.CaptureCallback() {

        @Override
        public void onCaptureStarted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, long timestamp, long frameNumber) {
        }

        @Override
        public void onCaptureProgressed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureResult partialResult) {
        }

        @Override
        public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) {
            if (!observableEmitter.isDisposed()) {
                observableEmitter.onNext(new CaptureSessionData(CaptureSessionEvents.ON_COMPLETED, session, request, result));
            }
        }

        @Override
        public void onCaptureFailed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureFailure failure) {
            if (!observableEmitter.isDisposed()) {
                observableEmitter.onError(new CameraCaptureFailedException(failure));
            }
        }

        @Override
        public void onCaptureSequenceCompleted(@NonNull CameraCaptureSession session, int sequenceId, long frameNumber) {
        }

        @Override
        public void onCaptureSequenceAborted(@NonNull CameraCaptureSession session, int sequenceId) {
        }
    };
}

Из всех этих сообщений нам интересны onCaptureCompleted/onCaptureFailed, остальные события игнорируем. Если они понадобятся вам в ваших проектах, их несложно добавить.


Теперь всё готово для создания Observable.


    static Observable<CaptureSessionData> fromSetRepeatingRequest(@NonNull CameraCaptureSession captureSession, @NonNull CaptureRequest request) {
        return Observable
            .create(observableEmitter -> captureSession.setRepeatingRequest(request, createCaptureCallback(observableEmitter), null));
    }

capture


На самом деле, этот шаг полностью аналогичен предыдущему, только мы делаем не повторяющийся запрос, а единичный. Для этого воспользуемся методом CameraCaptureSession.capture.


    public abstract int capture(@NonNull CaptureRequest request,
            @Nullable CaptureCallback listener, @Nullable Handler handler)
            throws CameraAccessException;

Он принимает точно такие же параметры, так что мы сможем использовать функцию, определённую выше, для создания CaptureCallback.


    static Observable<CaptureSessionData> fromCapture(@NonNull CameraCaptureSession captureSession, @NonNull CaptureRequest request) {
        return Observable
            .create(observableEmitter -> captureSession.capture(request, createCaptureCallback(observableEmitter), null));
    }

Подготовка Surface


Cameara2 API позволяет в запросе передавать список Surface, которые будут использованы для записи данных с устройства. Нам потребуются два Surface:


  • для отображения preview на экране,
  • для записи снимка в JPEG-файл.

TextureView


Для отображения preview на экране мы воспользуемся TextureView. Для того чтобы получить Surface из TextureView, предлагается воспользоваться методом TextureView.setSurfaceTextureListener.
TextureView уведомит listener, когда Surface будет готов к использованию.


Давайте на этот раз создадим PublishSubject, который будет генерировать события, когда TextureView вызывает методы listener.


private final PublishSubject<SurfaceTexture> mOnSurfaceTextureAvailable = PublishSubject.create();

@Override
public void onCreate(@Nullable Bundle saveState){

    mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener(){
        @Override
        public void onSurfaceTextureAvailable(SurfaceTexture surface,int width,int height){
           mOnSurfaceTextureAvailable.onNext(surface);
        }
    });
    ...
}

Используя PublishSubject, мы избегаем возможных проблем с множественным subscribe. Мы устанавливаем SurfaceTextureListener один раз в onCreate и дальше живём спокойно. PublishSubject позволяет подписываться на него сколько угодно раз и раздает события всем подписавшимся.



При использовании Camera2 API существует тонкость, связанная с невозможностью явно задать размер изображения, – камера сама выбирает одно из поддерживаемых ею разрешений на основании размеров, переданных ей Surface. Поэтому придётся пойти на такой трюк: выясняем список поддерживаемых камерой размеров изображения, выбираем наиболее приглянувшийся и затем устанавливаем размер буфера в точности таким же.


private void setupSurface(@NonNull SurfaceTexture surfaceTexture) {
    surfaceTexture.setDefaultBufferSize(mCameraParams.previewSize.getWidth(), mCameraParams.previewSize.getHeight());
    mSurface = new Surface(surfaceTexture);
}

При этом, если мы хотим видеть изображение с сохранением пропорций, необходимо задать нужные пропорции нашему TextureView. Для этого мы его расширим и переопределим метод onMeasure:


public class AutoFitTextureView extends TextureView {

    private int mRatioWidth = 0;
    private int mRatioHeight = 0;
...

    public void setAspectRatio(int width, int height) {
        mRatioWidth = width;
        mRatioHeight = height;
        requestLayout();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        if (0 == mRatioWidth || 0 == mRatioHeight) {
            setMeasuredDimension(width, height);
        } else {
            if (width < height * mRatioWidth / mRatioHeight) {
                setMeasuredDimension(width, width * mRatioHeight / mRatioWidth);
            } else {
                setMeasuredDimension(height * mRatioWidth / mRatioHeight, height);
            }
        }
    }
}

Запись в файл


Для того чтобы сохранить изображение из Surface в файл, воспользуемся классом ImageReader.
Несколько слов о выборе размера для ImageReader. Во-первых, мы должны выбрать его из поддерживаемых камерой. Во-вторых, соотношение сторон должно совпадать с тем, что мы выбрали для preview.


Чтобы мы могли получать уведомления от ImageReader о готовности изображения, воспользуемся методом setOnImageAvailableListener


void setOnImageAvailableListener (ImageReader.OnImageAvailableListener listener, Handler handler)

Передаваемый listener реализует всего один метод onImageAvailable.
Каждый раз, когда Camera API будет записывать изображение в Surface, предоставленную нашим ImageReader, он будет вызывать этот колбек.


Сделаем эту операцию реактивной: создадим Observable, который будет генерировать сообщение каждый раз, когда ImageReader будет готов предоставить изображение.


@NonNull
public static Observable<ImageReader> createOnImageAvailableObservable(@NonNull ImageReader imageReader) {
    return Observable.create(subscriber -> {

        ImageReader.OnImageAvailableListener listener = reader -> {
            if (!subscriber.isDisposed()) {
                subscriber.onNext(reader);
            }
        };
        imageReader.setOnImageAvailableListener(listener, null);
        subscriber.setCancellable(() -> imageReader.setOnImageAvailableListener(null, null)); //remove listener on unsubscribe
    });
}

Обратите внимание, тут мы воспользовались методом ObservableEmitter.setCancellable, чтобы удалять listener, когда от Observable отписываются.


Запись в файл – длительная операция, сделаем её реактивной с помощью метода fromCallable.


@NonNull
public static Single<File> save(@NonNull Image image, @NonNull File file) {
    return Single.fromCallable(() -> {
        try (FileChannel output = new FileOutputStream(file).getChannel()) {
            output.write(image.getPlanes()[0].getBuffer());
            return file;
        }
        finally {
            image.close();
        }
    });
}

Теперь мы можем задать такую последовательность действий: когда в ImageReader появляется готовое изображение, мы записываем его в файл в рабочем потоке Schedulers.io(), затем переключаемся в UI thread и уведомляем UI о готовности файла.


private void initImageReader() {
    Size sizeForImageReader = CameraStrategy.getStillImageSize(mCameraParams.cameraCharacteristics, mCameraParams.previewSize);
    mImageReader = ImageReader.newInstance(sizeForImageReader.getWidth(), sizeForImageReader.getHeight(), ImageFormat.JPEG, 1);
    mCompositeDisposable.add(
        ImageSaverRxWrapper.createOnImageAvailableObservable(mImageReader)
            .observeOn(Schedulers.io())
            .flatMap(imageReader -> ImageSaverRxWrapper.save(imageReader.acquireLatestImage(), mFile).toObservable())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(file -> mCallback.onPhotoTaken(file.getAbsolutePath(), getLensFacingPhotoType()))
    );
}

Запускаем preview


Итак, мы основательно подготовились. Мы уже можем создавать Observable для основных асинхронных действий, которые требуются для работы приложения. Впереди самое интересное – конфигурирование реактивных потоков.


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


        Observable<Pair<CameraRxWrapper.DeviceStateEvents, CameraDevice>> cameraDeviceObservable = mOnSurfaceTextureAvailable
            .firstElement()
.doAfterSuccess(this::setupSurface)
.doAfterSuccess(__ -> initImageReader())
            .toObservable()
            .flatMap(__ -> CameraRxWrapper.openCamera(mCameraParams.cameraId, mCameraManager))
            .share();

Ключевым оператором здесь выступает flatMap.



В нашем случае при получении события о готовности SurfaceTexture он выполнит функцию openCamera и пустит события от созданного ей Observable дальше в реактивный поток.


Важно также понять, зачем в конце цепочки используется оператор share. Он эквивалентен цепочке операторов publish().refCount().



Если долго смотреть на эту Marble Diagram, то можно заметить, что результат весьма похож на результат использования PublishSubject. Действительно, мы решаем похожую проблему: если на наш Observable подпишутся несколько раз, мы не хотим каждый раз открывать камеру заново.


Для удобства давайте введём ещё пару Observable.


        Observable<CameraDevice> openCameraObservable = cameraDeviceObservable
            .filter(pair -> pair.first == CameraRxWrapper.DeviceStateEvents.ON_OPENED)
            .map(pair -> pair.second)
            .share();

        Observable<CameraDevice> closeCameraObservable = cameraDeviceObservable
            .filter(pair -> pair.first == CameraRxWrapper.DeviceStateEvents.ON_CLOSED)
            .map(pair -> pair.second)
            .share();

openCameraObservable будет генерировать события, когда камера будет успешно открыта, а closeCameraObservable – когда она будет закрыта.


Сделаем ещё один шаг: после успешного открытия камеры откроем сессию.


        Observable<Pair<CameraRxWrapper.CaptureSessionStateEvents, CameraCaptureSession>> createCaptureSessionObservable = openCameraObservable
            .flatMap(cameraDevice -> CameraRxWrapper
                .createCaptureSession(cameraDevice, Arrays.asList(mSurface, mImageReader.getSurface()))
            )
            .share();

И по аналогии создадим ещё пару Observable, сигнализирующих об успешном открытии или закрытии сессии.


        Observable<CameraCaptureSession> captureSessionConfiguredObservable = createCaptureSessionObservable
            .filter(pair -> pair.first == CameraRxWrapper.CaptureSessionStateEvents.ON_CONFIGURED)
            .map(pair -> pair.second)
            .share();

        Observable<CameraCaptureSession> captureSessionClosedObservable = createCaptureSessionObservable
            .filter(pair -> pair.first == CameraRxWrapper.CaptureSessionStateEvents.ON_CLOSED)
            .map(pair -> pair.second)
            .share();

Наконец, мы можем задать повторяющийся запрос для отображения preview.


        Observable<CaptureSessionData> previewObservable = captureSessionConfiguredObservable
            .flatMap(cameraCaptureSession -> {
                CaptureRequest.Builder previewBuilder = createPreviewBuilder(cameraCaptureSession, mSurface);
                return CameraRxWrapper.fromSetRepeatingRequest(cameraCaptureSession, previewBuilder.build());
            })
            .share();

Теперь достаточно выполнить previewObservable.subscribe() — и на экране появится живая картинка с камеры!


Небольшое отступление. Если схлопнуть все промежуточные Observable, то получится вот такая цепочка операторов:


        mOnSurfaceTextureAvailable
            .firstElement()
            .doAfterSuccess(this::setupSurface)
            .toObservable()
            .flatMap(__ -> CameraRxWrapper.openCamera(mCameraParams.cameraId, mCameraManager))
            .filter(pair -> pair.first == CameraRxWrapper.DeviceStateEvents.ON_OPENED)
            .map(pair -> pair.second)
            .flatMap(cameraDevice -> CameraRxWrapper
                .createCaptureSession(cameraDevice, Arrays.asList(mSurface, mImageReader.getSurface()))
            )
            .filter(pair -> pair.first == CameraRxWrapper.CaptureSessionStateEvents.ON_CONFIGURED)
            .map(pair -> pair.second)
            .flatMap(cameraCaptureSession -> {
                CaptureRequest.Builder previewBuilder = createPreviewBuilder(cameraCaptureSession, mSurface);
                return CameraRxWrapper.fromSetRepeatingRequest(cameraCaptureSession, previewBuilder.build());
            })
            .subscribe();

И этого достаточно для показа preview. Впечатляет, не так ли?


На самом деле, у этого решения есть проблемы с закрытием ресурсов, да и снимки пока делать нельзя. Я привёл его, чтобы была видна цепочка целиком. Все промежуточные Observable понадобятся нам при составлении более сложных сценариев поведения в будущем.


Для того чтобы у нас была возможность отписаться, необходимо сохранять возвращаемое методом subscribe Disposable. Удобнее всего пользоваться CompositeDisposable.


private final CompositeDisposable mCompositeDisposable = new CompositeDisposable();

    private void unsubscribe() {
        mCompositeDisposable.clear();
    }

В реальном коде я везде делаю mCompositeDisposable.add(...subscribe()), но сейчас я эти вызовы опускаю, чтобы вам было легче читать.


Как составлять запросы CaptureRequest


Внимательный читатель, конечно, уже заметил, что мы использовали метод createPreviewBuilder, который ещё не описывался. Давайте же посмотрим, что у него внутри.


 @NonNull
    CaptureRequest.Builder createPreviewBuilder(CameraCaptureSession captureSession, Surface previewSurface) throws CameraAccessException {
        CaptureRequest.Builder builder = captureSession.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
        builder.addTarget(previewSurface);
        setup3Auto(builder);
        return builder;
    }

Здесь мы пользуемся любезно предоставленным нам шаблоном запроса для preview, добавляем в него нашу Surface и говорим, что хотим Auto Focus, Auto Exposure и Auto White Balance (три A). Чтобы этого добиться, достаточно установить несколько флагов.


    private void setup3Auto(CaptureRequest.Builder builder) {
        // Enable auto-magical 3A run by camera device
        builder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO);

        Float minFocusDist = mCameraParams.cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE);

        // If MINIMUM_FOCUS_DISTANCE is 0, lens is fixed-focus and we need to skip the AF run.
        boolean noAFRun = (minFocusDist == null || minFocusDist == 0);

        if (!noAFRun) {
            // If there is a "continuous picture" mode available, use it, otherwise default to AUTO.
            int[] afModes = mCameraParams.cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES);
            if (contains(afModes, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)) {
                builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
            }
            else {
                builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);
            }
        }

        // If there is an auto-magical flash control mode available, use it, otherwise default to
        // the "on" mode, which is guaranteed to always be available.
        int[] aeModes = mCameraParams.cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES);
        if (contains(aeModes, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH)) {
            builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
        }
        else {
            builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);
        }

        // If there is an auto-magical white balance control mode available, use it.
        int[] awbModes = mCameraParams.cameraCharacteristics.get(CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES);
        if (contains(awbModes, CaptureRequest.CONTROL_AWB_MODE_AUTO)) {
            // Allow AWB to run auto-magically if this device supports this
            builder.set(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_AUTO);
        }
    }

Делаем снимок


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


private final PublishSubject<Object> mOnShutterClick = PublishSubject.create();

    public void takePhoto() {
        mOnShutterClick.onNext(this);
    }

Теперь набросаем план действий. Прежде всего мы хотим делать снимок тогда, когда уже начался preview (это значит, что всё готово для снимка). Для этого воспользуемся оператором combineLatest.


Observable.combineLatest(previewObservable, mOnShutterClick, (captureSessionData, o) -> captureSessionData)

Но он будет генерировать события постоянно при получении свежих событий от previewObservable, поэтому ограничимся первым событием.


                .firstElement().toObservable()

Дождёмся, пока сработают автофокус и автоэкспозиция.


                .flatMap(this::waitForAf)
                .flatMap(this::waitForAe)

И, наконец, сделаем снимок.


.flatMap(captureSessionData -> captureStillPicture(captureSessionData.session))

Цепочка операторов целиком выглядит так:


            Observable.combineLatest(previewObservable, mOnShutterClick, (captureSessionData, o) -> captureSessionData)
                .firstElement().toObservable()
                .flatMap(this::waitForAf)
                .flatMap(this::waitForAe)
                .flatMap(captureSessionData -> captureStillPicture(captureSessionData.session))
                .subscribe(__ -> {
                }, this::onError)

Посмотрим, что внутри captureStillPicture.


    @NonNull
    private Observable<CaptureSessionData> captureStillPicture(@NonNull CameraCaptureSession cameraCaptureSession) {
        return Observable
            .fromCallable(() -> createStillPictureBuilder(cameraCaptureSession.getDevice()))
            .flatMap(builder -> CameraRxWrapper.fromCapture(cameraCaptureSession, builder.build()));
    }

Здесь нам всё уже довольно знакомо: создаём запрос, запускаем capture – и ждём результат. Запрос конструируется из шаблона STILL_PICTURE, в него добавляется Surface для записи в файл, а также несколько волшебных флагов, сообщающих камере, что это ответственный запрос для сохранения изображения. Также задаётся информация о том, как ориентировать изображение в JPEG.


    @NonNull
    private CaptureRequest.Builder createStillPictureBuilder(@NonNull CameraDevice cameraDevice) throws CameraAccessException {
        final CaptureRequest.Builder builder;
        builder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
        builder.set(CaptureRequest.CONTROL_CAPTURE_INTENT, CaptureRequest.CONTROL_CAPTURE_INTENT_STILL_CAPTURE);
        builder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CameraMetadata.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE);
        builder.addTarget(mImageReader.getSurface());
        setup3Auto(builder);

        int rotation = mWindowManager.getDefaultDisplay().getRotation();
        builder.set(CaptureRequest.JPEG_ORIENTATION, CameraOrientationHelper.getJpegOrientation(mCameraParams.cameraCharacteristics, rotation));
        return builder;
    }

Закрываем ресурсы


Хорошие приложения всегда закрывают ресурсы, особенно такие дорогие, как камера. Давайте тоже по событию onPause будем всё закрывать.


       Observable.combineLatest(previewObservable, mOnPauseSubject, (state, o) -> state)
            .firstElement().toObservable()
            .doOnNext(captureSessionData -> captureSessionData.session.close())
            .flatMap(__ -> captureSessionClosedObservable)
            .doOnNext(cameraCaptureSession -> cameraCaptureSession.getDevice().close())
            .flatMap(__ -> closeCameraObservable)
            .doOnNext(__ -> closeImageReader())
            .subscribe(__ -> unsubscribe(), this::onError);

Здесь мы поочерёдно закрываем сессию и устройство, дожидаясь подтверждения от API.


Выводы


Мы создали приложение, которое умеет показывать живую картинку preview и делать снимки. То есть получили вполне рабочее приложение камеры. Нераскрытыми остались вопросы ожидания срабатывания автофокуса и автовыбора экспозиции. Ответы на них обязательно появятся в следующей части.


[Update: вторая часть тут]


С появлением RxJava разработчики получили в свои руки могучий инструмент по обузданию асинхронных API. Используя его грамотно, можно избежать Callback Hell и получить чистый, легко читаемый и расширяемый код. Делитесь вашими мыслями в комментариях!

Tags:
Hubs:
+59
Comments 9
Comments Comments 9

Articles

Information

Website
badoo.com
Registered
Founded
2006
Employees
501–1,000 employees
Location
Россия
Representative
Yuliya Telezhkina