기본 사용법
CameraView와 CameraController로 카메라 UI를 통합하는 방법.
시작하기 전에
이 가이드를 시작하기 전에 시작하기를 완료하세요. 라이선스(LicenseInfo)와 탐지 설정(DetectionConfiguration)을 준비해야 합니다.
1.4.0의 권장 통합 방식은 CameraView(프리뷰 + 기본 트래킹 UI를 그리는 FrameLayout 위젯) 와 CameraController(카메라·탐지 세션 엔진) 를 직접 사용하는 것입니다. Fragment 상속이 필요하지 않으므로 일반 View 화면, Jetpack Compose, 커스텀 UI에 모두 자연스럽게 통합됩니다.
Fragment 호스팅 방식(PetnowCameraFragment)을 이미 사용 중이라면 Fragment 방식(레거시) 문서를 참고하세요.
카메라 화면 배치
레이아웃 또는 Compose에 CameraView를 추가합니다.
컨트롤러 생성 및 연결
CameraController를 만들고 리스너를 등록한 뒤 CameraView에 연결합니다.
세션 시작
카메라 권한을 확인하고 initializeCamera() → startDetection()을 호출합니다.
결과 처리
촬영 완료/실패 콜백을 처리합니다.
리소스 정리
화면 종료 시 컨트롤러를 정리합니다.
Step 1: 카메라 화면 배치
CameraView는 FrameLayout을 상속한 위젯입니다. XML 레이아웃에 직접 추가하거나, Compose에서는 AndroidView로 감쌉니다.
XML 레이아웃
<io.petnow.ui.CameraView
android:id="@+id/camera_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />Jetpack Compose
import androidx.compose.ui.viewinterop.AndroidView
import io.petnow.ui.CameraView
AndroidView(
factory = { ctx -> CameraView(ctx) },
modifier = Modifier.fillMaxSize(),
update = { view -> view.controller = controller }, // Step 2의 controller
)CameraView는 카메라 프리뷰와 기본 트래킹 UI(코/얼굴 트래커)를 자동으로 렌더링합니다. 프리뷰 Surface도 내부에서 관리하므로, 컨트롤러만 연결하면 됩니다.
Step 2: 컨트롤러 생성 및 연결
CameraController를 생성하고, 탐지 이벤트 리스너를 등록한 뒤, CameraView에 연결합니다.
import io.petnow.ui.CameraController
import io.petnow.ui.CameraResult
import io.petnow.ui.config.LicenseInfo
import io.petnow.ui.status.DetectionStatus
import io.petnow.callback.PetnowCameraDetectionListenerV2
// lifecycleScope 등 화면 수명과 동일한 CoroutineScope를 사용하세요.
val controller = CameraController(
context.applicationContext,
LicenseInfo(apiKey = "YOUR_API_KEY"),
lifecycleScope,
)
controller.setDetectionListenerV2(object : PetnowCameraDetectionListenerV2 {
override fun onDetectionStatus(primaryDetectionStatus: DetectionStatus) {
// Step 5에서 구현 — 실시간 가이드
}
override fun onDetectionProgress(progress: Int) {
// Step 5에서 구현 — 진행률 0~100
}
override fun onDetectionFinished(result: CameraResult) {
// Step 4에서 구현 — 최종 결과
}
})
// CameraView에 연결하면 프리뷰가 시작됩니다.
cameraView.controller = controller 요소 이해하기
| 요소 | 설명 |
|---|---|
CameraController(context, license, scope) | 카메라·탐지 세션 엔진. scope는 세션 동안 유효한 CoroutineScope(예: lifecycleScope) |
LicenseInfo(apiKey) | 라이선스 정보(모니터링·메트릭용) |
setDetectionListenerV2(...) | 탐지 상태·진행률·최종 결과 콜백 등록 |
cameraView.controller = ... | 컨트롤러를 뷰에 연결(프리뷰 Surface 자동 공급) |
탐지 설정(species/purpose/enableFakeDetection)은 컨트롤러 생성이 아니라 다음 단계의 initializeCamera()에서 DetectionConfiguration으로 전달합니다.
Step 3: 세션 시작
카메라 권한을 확인한 뒤, initializeCamera()로 세션을 준비하고 startDetection()으로 탐지를 시작합니다. 두 호출은 분리되어 있습니다 — initializeCamera()는 카메라만 열고, 탐지는 startDetection()을 호출해야 시작됩니다.
import io.petnow.ui.config.DetectionConfiguration
import io.petnow.ui.config.DetectionPurpose
import io.petnow.ui.config.PetSpecies
import java.util.UUID
val configuration = DetectionConfiguration(
species = PetSpecies.DOG,
purpose = DetectionPurpose.PET_PROFILE_REGISTRATION,
enableFakeDetection = true,
)
// 서버에서 발급받은 captureSessionId (시작하기 참고)
val captureSessionId: UUID = /* 서버로부터 받은 captureSessionId */
lifecycleScope.launch {
// CameraView는 직접 권한을 요청하지 않습니다 — 호스트가 먼저 확인하세요.
if (!hasCameraPermission()) {
requestCameraPermission()
return@launch
}
controller.initializeCamera(configuration, captureSessionId)
controller.startDetection()
}권한은 호스트가 처리합니다
CameraView는 PetnowCameraFragment와 달리 카메라 권한을 자동으로 요청하지 않습니다. AndroidManifest.xml에 <uses-permission android:name="android.permission.CAMERA" />를 선언하고, 세션을 시작하기 전에 런타임 권한을 직접 요청하세요.
initializeCamera()는 suspend 함수로, 프리뷰 Surface가 준비되어 카메라가 처음 열릴 때까지 대기한 후 반환됩니다. 따라서 그 직후의 startDetection() 호출은 항상 안전합니다.
초기 카메라 방향
Android의 initializeCamera()에는 카메라 방향 인자가 없습니다. 항상 후면 카메라로 시작하며, 전/후면 전환은 switchCamera()로 토글합니다(전면으로 시작하는 옵션은 없음). iOS의 initializeCamera(initialPosition:)와 다른 점입니다.
Step 4: 촬영 결과 처리
탐지가 완료되면 onDetectionFinished(result: CameraResult)가 호출됩니다.
override fun onDetectionFinished(result: CameraResult) {
when (result) {
is CameraResult.Success -> {
// result.fingerprintImageFiles: 생체 인식용 이미지 (강아지=코, 고양이=얼굴)
// result.appearanceImageFiles: 외형(얼굴) 이미지
uploadImages(result.fingerprintImageFiles, result.appearanceImageFiles)
}
is CameraResult.Fail -> {
// 촬영 실패 — 재시도하거나 세션을 종료합니다
showRetryOrExitDialog()
}
}
}CameraResult 타입
sealed class CameraResult {
data class Success(
val fingerprintImageFiles: List<File>, // 생체 인식용 (강아지=코, 고양이=얼굴)
val appearanceImageFiles: List<File> // 외형(얼굴) 이미지
) : CameraResult()
data object Fail : CameraResult()
}이미지 파일은 앱의 캐시 디렉터리에 file:// 경로로 저장됩니다. 이 파일들을 앱 서버로 업로드한 뒤, 서버가 Petnow Server API로 등록/인증/식별을 수행하는 것이 권장 흐름입니다. 업로드 엔드포인트(/v2/fingerprints:upload·/v2/appearances:upload)와 전체 흐름은 서버 API – 생체 데이터를 참고하세요.
실패(Fail) 처리하기
CameraResult.Fail은 촬영 시간 내에 충분한 이미지를 확보하지 못했을 때 전달됩니다(예: 반려동물이 프레임을 벗어남, 조명 부적합 등).
is CameraResult.Fail -> {
AlertDialog.Builder(requireContext())
.setTitle("촬영 실패")
.setMessage("코무늬를 충분히 촬영하지 못했습니다. 다시 시도하시겠습니까?")
.setPositiveButton("재시도") { _, _ ->
controller.startDetection() // 같은 세션에서 처음부터 다시 시작
}
.setNegativeButton("취소") { _, _ -> navigateBack() }
.show()
}실패 시 서버 세션 관리
Fail을 수신해도 서버의 캡처 세션은 아직 열린 상태입니다. 재시도하지 않고 화면을 닫으면 서버가 약 5분 후 자동으로 세션을 종료(ABORTED) 처리합니다.
Step 5: 상태·진행률 관찰
진행률
override fun onDetectionProgress(progress: Int) {
// progress: 0~100
progressBar.progress = progress
}탐지 상태 (실시간 가이드)
onDetectionStatus로 전달되는 DetectionStatus는 sealed class입니다. Failed인 경우 reason으로 구체적인 원인을 알 수 있습니다.
override fun onDetectionStatus(primaryDetectionStatus: DetectionStatus) {
statusTextView.text = when (primaryDetectionStatus) {
DetectionStatus.NoObject -> "반려동물을 프레임에 맞춰주세요"
DetectionStatus.Processing -> "탐지 중..."
DetectionStatus.Detected -> "좋아요! 그대로 유지하세요"
DetectionStatus.Finished -> "촬영 완료"
is DetectionStatus.Failed -> when (primaryDetectionStatus.reason) {
DetectionFailureReason.TooClose -> "조금 멀리 떨어져주세요"
DetectionFailureReason.TooFarAway -> "조금 가까이 다가가주세요"
DetectionFailureReason.TooDark -> "밝은 곳으로 이동해주세요"
DetectionFailureReason.NotFrontFace -> "얼굴 정면을 비춰주세요"
else -> "자세를 조정해주세요"
}
}
}StateFlow로 관찰하기 (선택)
콜백 대신 state 프로퍼티가 반환하는 StateFlow<DetectionState>로 상태를 관찰할 수도 있습니다. Compose나 단방향 데이터 흐름에 유용합니다.
lifecycleScope.launch {
controller.state.collect { state ->
// state.progressPercent: 진행률 (0~100)
// state.detectionStatusList: 현재 프레임의 탐지 상태 목록
// state.currentDetectionResult: 코/얼굴 BoundingBox (DetectionResult?)
// state.isDetectionFinished: 탐지 완료 여부
}
}탐지 결과의 코/얼굴 위치(DetectionResult = nose/face BoundingBox)는 V2 리스너가 아니라 state.currentDetectionResult로 제공됩니다.
Step 6: 리소스 정리
화면이 종료될 때 컨트롤러를 정리하세요. finalizeCamera()는 카메라 세션을 멈추고(컨트롤러 재사용 가능), release()는 네이티브 자원(디텍터·사운드 풀)까지 완전히 해제합니다(종료).
override fun onDestroy() {
super.onDestroy()
controller.release() // 화면을 완전히 떠날 때
}Compose에서는 DisposableEffect의 onDispose에서 정리합니다.
DisposableEffect(Unit) {
onDispose { controller.release() }
}추가 기능
전/후면 카메라 전환
import io.petnow.ui.camera.cameraInfo
if (context.cameraInfo.isFrontCameraSupported) {
controller.switchCamera() // Result 반환
.onFailure { e -> Toast.makeText(context, "전환 실패: ${e.message}", Toast.LENGTH_SHORT).show() }
} else {
Toast.makeText(context, "전면 카메라를 지원하지 않는 기기입니다", Toast.LENGTH_SHORT).show()
}Detection 일시정지 / 재개
카메라 프리뷰는 유지한 채 탐지만 잠시 멈출 수 있습니다. 팁 다이얼로그를 표시하는 동안 유용합니다.
controller.pauseDetection() // 일시정지 (진행률·캡처 이미지 유지, 반환값 없음)
controller.resumeDetection() // 일시정지 시점부터 재개 (Result 반환)재촬영 (처음부터)
controller.startDetection() // 진행률을 0으로 리셋하고 새 탐지 세션 시작 (Result 반환)startDetection / pauseDetection / resumeDetection 차이
startDetection()— 진행률을 0으로 리셋하고 새 탐지 세션 시작(재촬영).Result반환.pauseDetection()— 탐지만 일시정지. 프리뷰·진행률 유지. 반환값 없음.resumeDetection()— 일시정지 시점부터 이어서 재개.Result반환.
에러 처리
initializeCamera()(suspend)는 초기화 실패를 sealed class PetnowUIError로 던지고, startDetection()·resumeDetection()·switchCamera()는 Result<Unit>을 반환합니다. 각각 처리하세요.
initializeCamera()는 카메라를 열기 전에 API 키를 서버에서 검증합니다(프로세스당 키별 1회 — 이미 검증된 키는 네트워크를 다시 타지 않음). 거부된 키는 PetnowUIError.InvalidLicense로 실패하며, 이 경우 카메라는 열리지 않고 탐지도 시작되지 않습니다.
import io.petnow.ui.PetnowCaptureException
import io.petnow.ui.PetnowUIError
import kotlinx.coroutines.CancellationException
lifecycleScope.launch {
try {
controller.initializeCamera(configuration, captureSessionId)
} catch (c: CancellationException) {
throw c // 화면 전환/재설정으로 인한 취소 — 정상 생명주기이므로 재던짐
} catch (e: PetnowUIError) {
when (e) { // sealed — 세 케이스로 모든 초기화 실패를 망라
is PetnowUIError.InvalidLicense -> { /* API 키가 서버에서 거부됨 — 키 확인 */ }
is PetnowUIError.PermissionDenied -> { /* 카메라 권한 누락 — 권한 요청/설정 안내 */ }
is PetnowUIError.CameraOpenFailed -> { /* 카메라 열기 실패 — 원인은 e.cause */ }
}
return@launch
}
controller.startDetection()
.onSuccess { /* 탐지 시작됨 */ }
.onFailure { e -> handleDetectionError(e) }
}
private fun handleDetectionError(e: Throwable) {
when (e) {
is PetnowCaptureException.InvalidState -> { /* 컨트롤러가 아직 초기화되지 않음 */ }
is PetnowCaptureException.AlreadyCapturing -> { /* 이미 탐지가 진행 중 */ }
is PetnowCaptureException.NotSupported -> { /* 현재 기기/설정에서 미지원 */ }
else -> { /* 기타(PetnowCaptureException.Unknown 등) */ }
}
}예외·결과 타입
| 호출 | 실패 표현 | 주요 타입 |
|---|---|---|
initializeCamera() | 예외 throw | PetnowUIError(InvalidLicense 라이선스 거부 / PermissionDenied 권한 누락 / CameraOpenFailed 카메라 열기 실패 — 원인은 cause), CancellationException(정상 취소) |
startDetection() / resumeDetection() | Result.failure | PetnowCaptureException(InvalidState/AlreadyCapturing/NotSupported/Unknown) |
switchCamera() | Result.failure | PetnowCaptureException(NotSupported 전면 카메라 미지원 / InvalidState 미초기화 / Unknown 기타 — 원인은 cause) |
PetnowUIError(io.petnow.ui.PetnowUIError)와 PetnowCaptureException(io.petnow.ui.PetnowCaptureException)은 모두 sealed class입니다 — when으로 케이스를 분기하세요. PetnowUIError는 iOS의 PetnowUIError와 같은 케이스 구성이라(invalidLicense/permissionDenied), 두 플랫폼에서 같은 이름으로 초기화 실패를 처리할 수 있습니다.
모범 사례
API 키 안전하게 관리하기
// ❌ 나쁜 예: 코드에 하드코딩
val apiKey = "sk_live_abc123..."
// ✅ 좋은 예: BuildConfig 또는 local.properties 사용
val apiKey = BuildConfig.PETNOW_API_KEYAPI 키를 소스 코드에 직접 작성하지 마세요. local.properties 또는 CI/CD 환경변수를 통해 주입하는 것을 권장합니다.
리소스 정리하기
화면을 떠날 때 반드시 controller.release()를 호출하세요. 호출하지 않으면 카메라·네이티브 디텍터 자원이 해제되지 않아 다음 세션에서 문제가 생길 수 있습니다.
문제 해결
Q. 카메라가 시작되지 않아요
A. 다음을 확인하세요:
AndroidManifest.xml에 카메라 권한(CAMERA) 선언 여부- 세션을 시작하기 전에 런타임 권한을 요청했는지 (
CameraView는 직접 요청하지 않습니다) cameraView.controller = controller로 컨트롤러를 연결했는지initializeCamera()후startDetection()을 호출했는지
Q. 촬영이 계속 실패해요
A. 촬영 환경을 점검하세요:
- 밝은 실내 조명 아래에서 촬영 (직사광선·그림자 회피)
- 카메라와 반려동물 코 사이 거리 30~50 cm 유지
- 반려동물이 가만히 있도록 간식 등으로 유도
onDetectionStatus로 전달되는 상태를 UI에 표시하여 사용자에게 가이드 제공
Q. 화면을 전환했더니 프리뷰가 멈춰요
A. 컨트롤러를 정리하지 않고 화면을 떠나면 다음 세션에서 카메라가 열리지 않을 수 있습니다. 화면 종료 시 controller.release()를 호출하세요.
다음 단계
기본 사용법을 익혔다면 다음 문서를 참고하세요:
- 완전 커스텀 UI -
CameraController+ 직접 만든 SurfaceView로 100% 커스텀 UI 구현 - 커스터마이즈 -
CameraView가이드 UI 커스터마이즈 - 사운드 가이드 - 사운드 재생 설정
- Fragment 방식 (레거시) - 기존
PetnowCameraFragment통합 방식