Это вторая часть статьи, в которой я показываю, как использование RxJava2 помогает строить логику поверх асинхронного API. В качестве такого интерфейса я выбрал Android Camera2 API (и не пожалел!). Этот API не только асинхронен, но и таит в себе неочевидные особенности реализации, которые нигде толком не описаны. Так что статья нанесет читателю двойную пользу.
Для кого этот пост? Я рассчитываю, что читатель — умудрённый опытом, но всё ещё любознательный Android-разработчик. Очень желательны базовые знания о реактивном программировании (хорошее введение — здесь) и понимание Marble Diagrams. Пост будет полезен тем, кто хочет проникнуться реактивным подходом, а также тем, кто планирует использовать Camera2 API в своих проектах.
Исходники проекта можно найти на GitHub.
Чтение первой части обязательно!
Постановка задачи
В конце первой части я пообещал, что раскрою вопрос ожидания срабатывания автофокуса/ автоэкспозиции.
Напомню, цепочка операторов выглядела так:
Observable.combineLatest(previewObservable, mOnShutterClick, (captureSessionData, o) -> captureSessionData)
.firstElement().toObservable()
.flatMap(this::waitForAf)
.flatMap(this::waitForAe)
.flatMap(captureSessionData -> captureStillPicture(captureSessionData.session))
.subscribe(__ -> {}, this::onError)
Итак, что же мы хотим от методов
waitForAe
и waitForAf
? Чтобы были запущены процессы автофокусировки/ автоэкспозиции, а по их завершении мы бы получили уведомление о готовности к снимку. Для этого нужно, чтобы оба метода возвращали
Observable
, который испускает событие, когда камера сообщает о том, что процесс схождения сработал (чтобы не повторять слова «автофокусировка» и «автоэкспозиция», далее я буду использовать слово «схождение»). Но как запустить и проконтролировать этот процесс?Те самые неочевидные особенности конвейера Camera2 API
Сначала я думал, что достаточно вызвать
capture
c нужными флажками и дождаться в переданном CaptureCallback
вызова onCaptureCompleted
. Вроде логично: запустили запрос, дождались выполнения — значит, запрос выполнен. И такой код даже ушел в продакшен.
Но потом мы заметили, что на некоторых устройствах в очень тёмных условиях даже при срабатывающей вспышке фотографии получаются не в фокусе и затемнённые. При этом системная камера работала отлично, правда, у неё уходило гораздо больше времени на подготовку к снимку. Я начал подозревать, что в моем случае автофокус к моменту
onCaptureCompleted
не успевает сфокусироваться. Для проверки своего тезиса я добавил задержку в секунду — и снимки стали получаться! Понятно, что таким решением я не мог быть доволен, и начал искать, как на самом деле можно понять, что автофокус сработал и можно продолжать. Документации на эту тему найти не удалось, и мне пришлось обратиться к сорсам системной камеры, благо они доступны как часть Android Open Source Project. Код оказался на редкость нечитаемым и запутанным, пришлось добавлять логирование и анализировать логи камеры при съёмке в темноте. И я обнаружил, что после capture с нужными флажками системная камера вызывает
setRepeatingRequest
для продолжения превью и ждёт, пока в колбек не придёт onCaptureCompleted
с определённым набором флагов в TotalCaptureResult
. Нужный ответ мог прийти через несколько onCaptureCompleted
! Когда я осознал эту особенность, поведение Camera2 API стало казаться логичным. Но сколько потребовалось приложить усилий, чтобы найти эти сведения! Что ж, теперь можно перейти к описанию решения.
Итак, наш план действий:
- вызов capture с флагами, запускающими процесс схождения;
- вызов
setRepeatingRequest
для продолжения превью; - получение уведомлений от обоих методов;
- ожидание в результатах уведомлений
onCaptureCompleted
свидетельств того, что процесс схождения завершён.
Поехали!
Флажки
Создадим класс
ConvergeWaiter
со следующими полями:private final CaptureRequest.Key<Integer> mRequestTriggerKey;
private final int mRequestTriggerStartValue;
Это ключ и значение флажка, который запустит необходимый процесс схождения при вызове
capture
.Для автофокуса это будут
CaptureRequest.CONTROL_AF_TRIGGER
и CameraMetadata.CONTROL_AF_TRIGGER_START
соответственно. Для автоэкспозиции — CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER
и CameraMetadata.CONTROL_AE_PRECAPTURE_TRIGGER_START
соответственно.private final CaptureResult.Key<Integer> mResultStateKey;
private final List<Integer> mResultReadyStates;
А это ключ и набор ожидаемых значений флага из результата
onCaptureCompleted
. Когда мы увидим одно из ожидаемых значений ключа, можно считать, что процесс схождения выполнен.Для автофокуса значение ключа
CaptureResult.CONTROL_AF_STATE
, список значений: CaptureResult.CONTROL_AF_STATE_INACTIVE,
CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED,
CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED;
для автоэкспозиции значение ключа
CaptureResult.CONTROL_AE_STATE
, список значений:CaptureResult.CONTROL_AE_STATE_INACTIVE,
CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED,
CaptureResult.CONTROL_AE_STATE_CONVERGED,
CaptureResult.CONTROL_AE_STATE_LOCKED.
Не спрашивайте меня, как я это выяснил! Теперь мы можем создавать инстансы
ConvergeWaiter
для автофокуса и экспозиции, для этого сделаем фабрику:static class Factory {
private static final List<Integer> afReadyStates = Collections.unmodifiableList(
Arrays.asList(
CaptureResult.CONTROL_AF_STATE_INACTIVE,
CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED,
CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED
)
);
private static final List<Integer> aeReadyStates = Collections.unmodifiableList(
Arrays.asList(
CaptureResult.CONTROL_AE_STATE_INACTIVE,
CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED,
CaptureResult.CONTROL_AE_STATE_CONVERGED,
CaptureResult.CONTROL_AE_STATE_LOCKED
)
);
static ConvergeWaiter createAutoFocusConvergeWaiter() {
return new ConvergeWaiter(
CaptureRequest.CONTROL_AF_TRIGGER,
CameraMetadata.CONTROL_AF_TRIGGER_START,
CaptureResult.CONTROL_AF_STATE,
afReadyStates
);
}
static ConvergeWaiter createAutoExposureConvergeWaiter() {
return new ConvergeWaiter(
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
CameraMetadata.CONTROL_AE_PRECAPTURE_TRIGGER_START,
CaptureResult.CONTROL_AE_STATE,
aeReadyStates
);
}
}
capture
/setRepeatingRequest
Для вызова
capture
/setRepeatingRequest
нам потребуются: - открытая ранее
CameraCaptureSession
, которая доступна вCaptureSessionData
;
CaptureRequest
, который мы создадим, используяCaptureRequest.Builder.
Создадим метод
Single<CaptureSessionData> waitForConverge(@NonNull CaptureSessionData captureResultParams, @NonNull CaptureRequest.Builder builder)
Во второй параметр мы будем передавать
builder
, настроенный для превью. Поэтому CaptureRequest
для превью можно создать сразу вызовом CaptureRequest previewRequest = builder.build();
Для создания
CaptureRequest
для запуска процедуры схождения добавим в builder
флаг, который запустит необходимый процесс схождения:builder.set(mRequestTriggerKey, mRequestTriggerStartValue);
CaptureRequest triggerRequest = builder.build();
И воспользуемся нашими методами для получения
Observable
из методов capture
/setRepeatingRequest
:Observable<CaptureSessionData> triggerObservable = CameraRxWrapper.fromCapture(captureResultParams.session, triggerRequest);
Observable<CaptureSessionData> previewObservable = CameraRxWrapper.fromSetRepeatingRequest(captureResultParams.session, previewRequest);
Формирование цепочки операторов
Теперь мы можем сформировать реактивный поток, в котором будут события от обоих Observable c помощью оператора
merge
.Observable<CaptureSessionData> convergeObservable = Observable
.merge(previewObservable, triggerObservable)
Полученный
convergeObservable
будет испускать события с результатами вызовов onCaptureCompleted
.Нам необходимо дождаться момента, когда
CaptureResult
, переданный в этот метод, будет содержать ожидаемое значение флага. Для этого создадим функцию, которая принимает CaptureResult
и возвращает true
если в нём есть ожидаемое значение флага:private boolean isStateReady(@NonNull CaptureResult result) {
Integer aeState = result.get(mResultStateKey);
return aeState == null || mResultReadyStates.contains(aeState);
}
Проверка на
null
нужна для кривых реализаций Camera2 API, чтобы не зависнуть в ожидании навеки.Теперь мы можем воспользоваться оператором
filter
, чтобы дождаться события, для которого выполнено isStateReady
: .filter(resultParams -> isStateReady(resultParams.result))
Нам интересно только первое такое событие, поэтому добавляем
.first(captureResultParams);
Полностью реактивный поток выглядит так:
Single<CaptureSessionData> convergeSingle = Observable
.merge(previewObservable, triggerObservable)
.filter(resultParams -> isStateReady(resultParams.result))
.first(captureResultParams);
На случай если процесс схождения затягивается слишком долго или что-то пошло не так, введём таймаут:
private static final int TIMEOUT_SECONDS = 3;
Single<CaptureSessionData> timeOutSingle = Single
.just(captureResultParams)
.delay(TIMEOUT_SECONDS, TimeUnit.SECONDS, AndroidSchedulers.mainThread());
Оператор
delay
переиспускает события с заданной задержкой. По умолчанию он это делает в потоке, принадлежащем computation scheduler, поэтому мы перекидываем его в Main Thread с помощью последнего параметра.Теперь скомбинируем
convergeSingle
и timeOutSingle
, и кто первый испустит событие — тот и победил:return Single
.merge(convergeSingle, timeOutSingle)
.firstElement()
.toSingle();
Полный код функции:
@NonNull
Single<CaptureSessionData> waitForConverge(@NonNull CaptureSessionData captureResultParams, @NonNull CaptureRequest.Builder builder) {
CaptureRequest previewRequest = builder.build();
builder.set(mRequestTriggerKey, mRequestTriggerStartValue);
CaptureRequest triggerRequest = builder.build();
Observable<CaptureSessionData> triggerObservable = CameraRxWrapper.fromCapture(captureResultParams.session, triggerRequest);
Observable<CaptureSessionData> previewObservable = CameraRxWrapper.fromSetRepeatingRequest(captureResultParams.session, previewRequest);
Single<CaptureSessionData> convergeSingle = Observable
.merge(previewObservable, triggerObservable)
.filter(resultParams -> isStateReady(resultParams.result))
.first(captureResultParams);
Single<CaptureSessionData> timeOutSingle = Single
.just(captureResultParams)
.delay(TIMEOUT_SECONDS, TimeUnit.SECONDS, AndroidSchedulers.mainThread());
return Single
.merge(convergeSingle, timeOutSingle)
.firstElement()
.toSingle();
}
waitForAf
/waitForAe
Основная часть работы сделана, осталось лишь создать инстансы:
private final ConvergeWaiter mAutoFocusConvergeWaiter = ConvergeWaiter.Factory.createAutoFocusConvergeWaiter();
private final ConvergeWaiter mAutoExposureConvergeWaiter = ConvergeWaiter.Factory.createAutoExposureConvergeWaiter();
и использовать их:
private Observable<CaptureSessionData> waitForAf(@NonNull CaptureSessionData captureResultParams) {
return Observable
.fromCallable(() -> createPreviewBuilder(captureResultParams.session, mSurface))
.flatMap(
previewBuilder -> mAutoFocusConvergeWaiter
.waitForConverge(captureResultParams, previewBuilder)
.toObservable()
);
}
@NonNull
private Observable<CaptureSessionData> waitForAe(@NonNull CaptureSessionData captureResultParams) {
return Observable
.fromCallable(() -> createPreviewBuilder(captureResultParams.session, mSurface))
.flatMap(
previewBuilder -> mAutoExposureConvergeWaiter
.waitForConverge(captureResultParams, previewBuilder)
.toObservable()
);
}
Основной момент тут — использование оператора
fromCallable
. Может возникнуть соблазн использовать оператор just
. Например, так:just(createPreviewBuilder(captureResultParams.session, mSurface)).
Но в данном случае функция
createPreviewBuilder
будет вызвана прямо в момент вызова waitForAf
, а мы хотим, чтобы она была вызвана, только когда появится подписка на наш Observable
. Заключение
Как известно, самая ценная часть любой статьи на Хабре — комментарии! Поэтому я призываю вас активно делиться своими соображениями, замечаниями, ценными знаниями и ссылками на более удачные имплементации в комментариях.
Исходники проекта можно найти на GitHub. Пулреквесты приветствуются!