Android Lollipop (API 21) 버전 부터 기존 Camera API는 deprecated
되고 Camera2 API가 적용되었는데, Pipeline Stream 기반으로 기존에 비해 세밀한 세팅이 가능하고 비동기 루틴이 많아 Google의 샘플 소스 android-Camera2Basic을 봐서는 분석이 쉽지 않다. Google I/O 2014 프레젠테이션에서 Camera2 API에 대한 적용 흐름이 잘 설명되어 있어 해당 루틴을 기준으로 분석 및 이해를 목적으로 코드를 작성하였으며 실제 프로젝트에 적용하기에는 적절치 않다.
Architecture
source: https://source.android.com/devices/camera/index.html#architecture
Camera HAL(Hardware Abstraction Layer)에서 Application Framework단 사이에 구현된 스택. 왼쪽의 기존 Camera API와 비교하여 CameraService단에 Callback, Listener C++ Binder Interface가 보인다.
source: https://source.android.com/devices/camera/camera3_requests_hal.html
코드를 통해 다시 설명하겠지만, App - Camera2 API - HAL(Hardware Abstraction Layer)모델의 API 적용 프로세스를 표현한 그림.
Process
source: https://www.youtube.com/watch?v=92fgcUNCHic&feature=youtu.be&t=2130
Google I/O 2014 프레젠테이션에서 캡쳐한 Camera2 API 시퀀스. 다음은 영상의 프로세스 시퀸스 다이어그램.
- CameraManager
- 사용가능한 카메라를 나열하고,
CameraDevice
를 취득하기 위한 Camera2 API의 첫번째 클래스.
- 사용가능한 카메라를 나열하고,
- CameraCharacteristics
CameraManager
에 의에 나열된 Camera 하드웨어, 사용가능 세팅등에 대한 정보 취득.
- CameraDevice
- 실질적인 해당 카메라를 나타내는 클래스.
CameraManager
에 의해 비동기 콜백으로 취득.
- CameraCaptureSession
CameraDevice
에 의해 이미지 캡쳐를 위한 세션 연결.- 해당 세션이 연결될
surface
를 전달.
- CaptureRequest
CameraDevice
에 의해Builder
패턴으로 생성하며, 단일 이미지 캡쳐를 위한 하드웨어 설정(센서, 렌즈, 플래쉬) 및 출력 버퍼등의 정보(immutable).- 해당 리퀘스트가 연결될 세션의
surface
를 타겟으로 지정.
- CaptureResult
CaptureRequest
가 수행되고 비동기CameraCaptureSession.CaptureCallback
으로 취득.- 해당 세션의 리퀘스트 정보 뿐만 아니라 캡쳐 이미지의
Metadata
정보도 포함.
Code Project
분석 및 이해를 용이하게 하기 위해 Camera2 APIs는 클래스 파일(Camera2APIs.java
)하나만 작성하여 몰아 넣고 MainActivity
에서 해당 클래스만 사용.
AndroidManifest.xml
카메라 디바이스 접근을 위한 권한. Android 6.0 이상에서는 권한요청 코드(requestPermission)가 추가되어야 한다.
<uses-permission android:name="android.permission.CAMERA" />
activity_main.xml
Preview 화면을 위한 TextureView
추가. CaptureSession
의 surface
로 사용.
<TextureView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/textureView"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true" />
Camera2APIs.java
CameraManager
카메라 시스템 서비스 매니저 리턴.
public CameraManager CameraManager_1(Activity activity) {
CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
return cameraManager;
}
CameraCharacteristics
사용가능한 카메라 리스트를 가져와 후면 카메라(LENS_FACING_BACK
) 사용하여 해당 cameraId
리턴.
StreamConfiguratonMap
은 CaptureSession
을 생성할때 surfaces
를 설정하기 위한 출력 포맷 및 사이즈등의 정보를 가지는 클래스.
사용가능한 출력 사이즈중 가장 큰 사이즈 선택.
public String CameraCharacteristics_2(CameraManager cameraManager) {
try {
for (String cameraId : cameraManager.getCameraIdList()) {
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
if (characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_BACK) {
StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
Size[] sizes = map.getOutputSizes(SurfaceTexture.class);
mCameraSize = sizes[0];
for (Size size : sizes) {
if (size.getWidth() > mCameraSize.getWidth()) {
mCameraSize = size;
}
}
return cameraId;
}
}
} catch (CameraAccessException e) {
e.printStackTrace();
}
return null;
}
참고로, INFO_SUPPORTED_HARDWARE_LEVEL키값으로 카메라 디바이스의 레벨을 알 수 있는데 LEGACY < LIMITED < FULL < LEVEL_3 순으로 고성능이며 더 세밀한 카메라 설정이 가능하다. LEGACY 디바이스의 경우 구형 안드로이드 단말 호환을 위해 Camera2 API는 기존 Camera API의 인터페이스에 불과하다. 즉, 프레임 단위 컨트롤 등의 Camera2 기능은 사용할 수 없다.
CameraDevice
비동기 콜백 CameraDevice.StateCallback
onOpened()
로 취득. null
파라미터는 MainThread를 이용하고, 작성한 Thread Handler
를 넘겨주면 해당 Thread로 콜백이 떨어진다. 비교적 딜레이가 큰(~500ms) 작업이라 Thread 권장.
public void CameraDevice_3(CameraManager cameraManager, String cameraId) {
try {
cameraManager.openCamera(cameraId, mCameraDeviceStateCallback, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
onOpened()
에서 취득한 CameraDevice
로 CaptureSession
, CaptureRequest
가 이뤄지는데 Camera2 APIs 처리과정을 MainActivity
에서 일원화하여 표현하기 위해 인터페이스로 처리.
private CameraDevice.StateCallback mCameraDeviceStateCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice camera) {
mCameraDevice = camera;
mInterface.onCameraDeviceOpened(camera, mCameraSize);
}
@Override
public void onDisconnected(@NonNull CameraDevice camera) {
camera.close();
}
@Override
public void onError(@NonNull CameraDevice camera, int error) {
camera.close();
}
};
CaptureSession
일단 세션이 생성(비동기)된 후에는 해당 CameraDevice
에서 새로운 세션이 생성되거나 종료하기 이전에는 유효.
public void CaptureSession_4(CameraDevice cameraDevice, Surface surface) {
try {
cameraDevice.createCaptureSession(Collections.singletonList(surface), mCaptureSessionCallback, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
생성된 세션에 카메라 프리뷰를 위한 CaptureRequest
정보 설정. 프리뷰 화면은 연속되는 이미지가 보여지기 때문에 CONTROL_AF_MODE_CONTINUOUS_PICTURE
로 포커스를 지속적으로 맞추고, setRepeatingRequest
로 해당 세션에 설정된 CaptureRequest
세팅으로 이미지를 지속적으로 요청.
setRepeatingRequest
의 null
파라미터는 MainThread를 사용하고, 작성한 Thread Handler
를 넘겨주면 해당 Thread로 콜백이 떨어진다. 프리뷰 화면은 지속적으로 화면 캡쳐가 이뤄지기 때문에, MainThread 사용 시 Frame drop이 발생할 수 있다. Background Thread 사용 권장.
Google의 android-Camera2Basic샘플 코드에서는 CaptureRequest
를 먼저 수행하는데, 여기서는 Google 프레젠테이션 자료 및 모델 그림의 프로세스를 기준으로 작성하기 위해 CaptureSession
을 먼저 수행.
private CameraCaptureSession.StateCallback mCaptureSessionCallback = new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(CameraCaptureSession cameraCaptureSession) {
try {
mCaptureSession = cameraCaptureSession;
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
cameraCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), mCaptureCallback, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@Override
public void onConfigureFailed(CameraCaptureSession cameraCaptureSession) {
}
};
캡쳐된 이미지 정보 및 Metadata
가 넘어오는데, 프리뷰에서는 딱히 처리할 작업은 없다. 사진 촬영의 경우라면, onCaptureCompleted()
에서 촬영이 완료되고 이미지가 저장되었다는 메세지를 띄우는 시점. 캡쳐 이미지와 Metadata
매칭은 Timestamp
로 매칭가능하다.
private CameraCaptureSession.CaptureCallback mCaptureCallback = new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureProgressed(CameraCaptureSession session, CaptureRequest request, CaptureResult partialResult) {
super.onCaptureProgressed(session, request, partialResult);
}
@Override
public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
super.onCaptureCompleted(session, request, result);
}
};
CaptureRequest
카메라 프리뷰(CameraDevice.TEMPLATE_PREVIEW)를 위한 Builder 패턴의 CaptureRequest
생성. 예를 들어, 사진 촬영의 경우에는 CameraDevice.TEMPLATE_STILL_CAPTURE
로 리퀘스트를 설정한다. surface
는 해당 세션에 사용된 surface
를 타겟으로 설정.
public void CaptureRequest_5(CameraDevice cameraDevice, Surface surface) {
try {
mPreviewRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
mPreviewRequestBuilder.addTarget(surface);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
이상으로 프리뷰를 위한 Camera2 APIs 코드 작성은 완료.
Camera2APIs.java 전체 코드.
public class Camera2APIs {
interface Camera2Interface {
void onCameraDeviceOpened(CameraDevice cameraDevice, Size cameraSize);
}
private Camera2Interface mInterface;
private Size mCameraSize;
private CameraCaptureSession mCaptureSession;
private CameraDevice mCameraDevice;
private CaptureRequest.Builder mPreviewRequestBuilder;
public Camera2APIs(Camera2Interface impl) {
mInterface = impl;
}
public CameraManager CameraManager_1(Activity activity) {
CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
return cameraManager;
}
public String CameraCharacteristics_2(CameraManager cameraManager) {
try {
for (String cameraId : cameraManager.getCameraIdList()) {
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
if (characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_BACK) {
StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
Size[] sizes = map.getOutputSizes(SurfaceTexture.class);
mCameraSize = sizes[0];
for (Size size : sizes) {
if (size.getWidth() > mCameraSize.getWidth()) {
mCameraSize = size;
}
}
return cameraId;
}
}
} catch (CameraAccessException e) {
e.printStackTrace();
}
return null;
}
private CameraDevice.StateCallback mCameraDeviceStateCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice camera) {
mCameraDevice = camera;
mInterface.onCameraDeviceOpened(camera, mCameraSize);
}
@Override
public void onDisconnected(@NonNull CameraDevice camera) {
camera.close();
}
@Override
public void onError(@NonNull CameraDevice camera, int error) {
camera.close();
}
};
public void CameraDevice_3(CameraManager cameraManager, String cameraId) {
try {
cameraManager.openCamera(cameraId, mCameraDeviceStateCallback, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
private CameraCaptureSession.StateCallback mCaptureSessionCallback = new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(CameraCaptureSession cameraCaptureSession) {
try {
mCaptureSession = cameraCaptureSession;
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
cameraCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), mCaptureCallback, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@Override
public void onConfigureFailed(CameraCaptureSession cameraCaptureSession) {
}
};
public void CaptureSession_4(CameraDevice cameraDevice, Surface surface) {
try {
cameraDevice.createCaptureSession(Collections.singletonList(surface), mCaptureSessionCallback, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
public void CaptureRequest_5(CameraDevice cameraDevice, Surface surface) {
try {
mPreviewRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
mPreviewRequestBuilder.addTarget(surface);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
private CameraCaptureSession.CaptureCallback mCaptureCallback = new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureProgressed(CameraCaptureSession session, CaptureRequest request, CaptureResult partialResult) {
super.onCaptureProgressed(session, request, partialResult);
}
@Override
public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
super.onCaptureCompleted(session, request, result);
}
};
public void closeCamera() {
if (null != mCaptureSession) {
mCaptureSession.close();
mCaptureSession = null;
}
if (null != mCameraDevice) {
mCameraDevice.close();
mCameraDevice = null;
}
}
}
MainActivity.java
준비
TextureView
의 surface
가 사용가능할 때 카메라 오픈을 위한 Listener
및 MainActivity에서 CaptureSession
, CaptureRequest
호출을 위한 Camera2Interface
설정.
public class MainActivity extends AppCompatActivity
implements Camera2APIs.Camera2Interface, TextureView.SurfaceTextureListener {
private TextureView mTextureView;
private Camera2APIs mCamera;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextureView = (TextureView)findViewById(R.id.textureView);
mTextureView.setSurfaceTextureListener(this);
mCamera = new Camera2APIs(this);
}
}
Open Camera
openCamera()
만 호출하면 5단계 과정이 전부 수행되며, 프리뷰가 이뤄진다.
private void openCamera() {
CameraManager cameraManager = mCamera.CameraManager_1(this);
String cameraId = mCamera.CameraCharacteristics_2(cameraManager);
mCamera.CameraDevice_3(cameraManager, cameraId);
}
@Override
public void onCameraDeviceOpened(CameraDevice cameraDevice, Size cameraSize) {
SurfaceTexture texture = mTextureView.getSurfaceTexture();
texture.setDefaultBufferSize(cameraSize.getWidth(), cameraSize.getHeight());
Surface surface = new Surface(texture);
mCamera.CaptureSession_4(cameraDevice, surface);
mCamera.CaptureRequest_5(cameraDevice, surface);
}
Surface Texture가 준비 완료된 콜백을 받으면, 카메라 오픈.
@Override
protected void onResume() {
super.onResume();
if (mTextureView.isAvailable()) {
openCamera();
} else {
mTextureView.setSurfaceTextureListener(this);
}
}
/* Surface Callbacks */
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i1) {
openCamera();
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int i, int i1) {
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
return true;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
}
Close Camera
private void closeCamera() {
mCamera.closeCamera();
}
@Override
protected void onPause() {
closeCamera();
super.onPause();
}
실행화면
References
- https://developer.android.com/reference/android/hardware/camera2/package-summary.html
- https://source.android.com/devices/camera/index.html
- https://www.youtube.com/watch?v=92fgcUNCHic&feature=youtu.be&t=2130
- https://github.com/googlesamples/android-Camera2Basic
- https://github.com/kotemaru/androidCamera2Sample
- https://github.com/shimoda-tomoaki/CameraTestProject
- https://github.com/mhidaka/Camera2App
댓글