Giới thiệu camera 2 API trong Android (phần 2)

Chuẩn bị cho Surface

API Camera2 cho phép danh sách Surface được truyền đi trong request. Những surface này sẽ nhận được dữ liệu từ thiết bị. Chúng ta cần 2 Surfaces:

  • Để hiển thị bản xem trước trên màn hình
  • Để viết hình ảnh vào một tệp jpeg

TextureView

Để hiển thị bản preview trên màn hình, chúng ta sẽ sử dụng TextureView. Để nhận Surface từ TextureView, nên sử dụng method sau: TextureView.setSurfaceTextureListener.

Void setSurfaceTextureListener (TextureView.SurfaceTextureListener listener)

TextureView thông báo cho người nghe khi surface đã sẵn sàng để sử dụng.

Lần này, hãy tạo PublishSubject, sẽ tạo ra các sự kiện khi TextureView gọi các phương thức nghe:

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);
        }
    });
    ...
}

Khi sử dụng PublishSubject, chúng ta tránh những vấn đề tiềm ẩn với nhiều đăng ký. Chúng ta sẽ thiết lập SurfaceTextureListener trong onCreate một lần và sống một cách hòa bình sau đó. PublishSubject có thể được đăng ký nhiều lần khi cần thiết, chuyển các sự kiện cho tất cả các subscriber.

Một lỗ hổng cụ thể trong việc sử dụng API Camera2 là bạn không thể xác định rõ kích thước của hình ảnh. Máy ảnh này chọn một trong các độ phân giải được hỗ trợ dựa trên kích thước của surface được gửi tới nó. Điều này có nghĩa là cần phải có thủ thuật sau đây: chúng ta có được danh sách các kích thước hình ảnh được hỗ trợ bởi máy ảnh, chọn một hình thích hợp nhất và sau đó đặt kích thước bộ đệm theo thông tin này.

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

Nếu muốn tiết kiệm tỷ lệ, chúng ta cần phải đặt tỷ lệ khung của TextureView. Đối với điều này chúng ta sẽ ghi đè lên phương pháp 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);
            }
        }
    }
}

Viết ra file

Để lưu một hình ảnh từ Surface vào tệp, chúng ta sẽ sử dụng lớp ImageReader.

Một vài từ về việc chọn kích thước cho ImageReader. Thứ nhất, chúng ta cần phải chọn nó từ danh sách những người được hỗ trợ bởi máy ảnh. Thứ hai, tỉ lệ co phải được chọn khớp với preview.

Để nhận thông báo từ ImageReader liên quan đến hình ảnh đang sẵn sàng, chúng ta sẽ sử dụng method:

ImageReader.setOnImageAvailableListener

void setOnImageAvailableListener (ImageReader.OnImageAvailableListener listener,
                Handler handler)

Listener sẽ chuyển chính xác 1 method:

void onImageAvailable (ImageReader reader)

Mỗi lần Camera API ghi một hình ảnh vào surface, được cung cấp bởi ImageReader của chúng ta, nó sẽ gọi đến callback này.

Hãy làm cho operation này phản ứng: chúng ta sẽ tạo ra một Observable, sẽ phát ra một sự kiện mỗi khi ImageReader sẵn sàng cung cấp một hình ảnh:

@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
    });
}

Hãy lưu ý rằng chúng ta đang sử dụng phương thức ObservableEmitter.setCancellable để xóa người nghe khi Observable đang được unsubscribe.

Lưu vào tệp là một hoạt động dài, vì vậy chúng ta hãy làm cho phản ứng này bằng cách sử dụng phương thức 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();
            }
        });
}

Bây giờ chúng ta có thể thiết lập chuỗi hành động này: khi một hình ảnh đã sẵn sàng xuất hiện trong ImageReader, chúng ta sẽ lưu lại nó là luồng Schedulers.io (), sau đó chuyển sang giao diện UI và thông báo cho UI rằng tập tin đã sẵn sàng:

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()))
    );
}

Launching preview

Vì vậy, bây giờ về cơ bản chúng ta đã sẵn sàng! Chúng ta đã có thể tạo ra các Observable cho các hành động không đồng bộ cơ bản, được yêu cầu cho ứng dụng để làm việc. Bây giờ cho thời điểm thú vị nhất - cấu hình các dòng phản ứng.

Khi khởi động, hãy mở máy ảnh sau khi SurfaceTexture đã sẵn sàng để sử dụng:

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

Ở đây, chúng ta sẽ dùng đến flatMap:

Trong trường hợp này, khi nhận được một sự kiện liên quan đến SurfaceTexture đang sẵn sàng, chức năng openCamera sẽ được thực hiện và phát ra các sự kiện từ các quan sát được tạo ra hơn nữa vào dòng phản ứng.

Cũng cần phải hiểu lý do tại sao chúng ta sử dụng toán tử chia sẻ ở cuối chuỗi. Nhà điều hành này tương đương với chuỗi nhà khai thác publish (). RefCount ().

Nếu bạn nhìn vào biểu đồ marble này trong một thời gian dài, bạn sẽ nhận thấy rằng kết quả rất giống với việc sử dụng PublishSubject. Thật vậy, chúng tôi đang giải quyết một vấn đề tương tự - nếu Observable của chúng tôi được đăng ký nhiều lần, chúng tôi không muốn mở lại máy ảnh mỗi lần.

Hãy giới thiệu một vài Observables khác cho thuận tiện.

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 sẽ phát ra một sự kiện khi camera thành công, và closeCameraObservable sẽ phát ra một sự kiện khi camera đóng.

Hãy đặt thêm một bước nữa: sau khi camera đã mở thành công, chúng ta sẽ mở session:

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

Trong một cách tương tự, chúng ta hãy tạo ra một vài Observables để báo hiệu rằng session này đã được mở hoặc đóng thành công.

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();

Cuối cùng, chúng ta có thể gửi yêu cầu lặp đi lặp lại để hiển thị bản preview:

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

Như vậy đã đủ để chạy:

previewObservable.subscribe()

Nguồn: https://techblog.badoo.com/blog/2017/06/07/reactive-selfies-with-camera2-api-on-android-part-1/