Как известно, 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 и делать снимки. То есть получили вполне рабочее приложение камеры. Нераскрытыми остались вопросы ожидания срабатывания автофокуса и автовыбора экспозиции и выбора ориентации файла. Ответы на них обязательно появятся в следующей части.
С появлением RxJava разработчики получили в свои руки могучий инструмент по обузданию асинхронных API. Используя его грамотно, можно избежать Callback Hell и получить чистый, легко читаемый и расширяемый код. Делитесь вашими мыслями в комментариях!
Комментарии (8)
jehy
08.06.2017 19:34А как вы решаете вопрос, если camera2 на устройстве ещё нет? Какой-то универсальный адаптер?
ArkadyGamza
09.06.2017 02:34+1В примере никак не решаем, он ведь про camera2 API.
В реальной жизни обычно создается 2 имплементации для первой и второй версии Camera API, реализующих общий интерфейс, выбор между ними в рантайме. (Причем я видел довольно нетривиальный алгоритм выбора. Для некоторых устройств выбирается camera1 api даже если это Lollipop и новее.) Если лениво делать поддержку camera1 API, то можно, например, запускать системную камеру для pre Lollipop. Ну или minSdkVersion=21 и только camera2 api.
Nethergrim
09.06.2017 10:411) RxJava immutable, а значит куча лишних объектов на каждый ивент в observable. Rx не подходит для интенсивной работы на андроиде, потому что GC будет сходить с ума. Говорю как истинный фанат реактивщины на андроиде.
2) Для лямбд уже не нужна Retrolambda
3) Enum, сами знаете. Intdef наше все
В остальном все хорошо, плюсую.artemgapchenko
09.06.2017 12:50+1Мне кажется, опасения насчет enum на Андроиде были валидны в самом начале существования Андроида, лет так семь назад, когда устройства были слабее, памяти было мало, и Dalvik был на самой заре своего существования. Сейчас отказываться от enum-ов — это экономия на спичках в большинстве случаев.
Virtyoz
16.06.2017 21:21Согласен, что это экономия на спичках. К тому же, Proguard оптимизирует использование простых enum: ответ на StackOverflow.
forceLain
А что на счет setRepeatingRequest и backpressure? Разве не нужно это обрабатывать? Как часто будут прилетать эти события?
ArkadyGamza
Observable, который создает функция fromSetRepeatingRequest является «горячим». Это может приводить к проблемам с операторами, которые накапливают события. В нашем случае мы первым делом к полученному Observable применяем цепочку операторов combineLatest.firstElement. CombineLatest держит только одно последнее событие для более быстрого Observable, переданного ему в качестве параметра. То есть он не накапливает элементы.