프로젝션된 컨텍스트를 사용하여 오디오 글라스 및 디스플레이 글라스의 하드웨어에 액세스

적용 가능한 XR 기기
이 가이드는 이러한 유형의 XR 기기를 위한 환경을 빌드하는 데 도움이 됩니다.
오디오 및
디스플레이 안경

필요한 권한을 요청하고 부여받은 후에는 앱이 오디오 글라스 또는 디스플레이 글라스의 하드웨어에 액세스할 수 있습니다. 휴대전화의 하드웨어 대신 스마트 안경의 하드웨어에 액세스하는 핵심은 프로젝션된 컨텍스트를 사용하는 것입니다.

코드가 실행되는 위치에 따라 예상 컨텍스트를 가져오는 두 가지 기본 방법이 있습니다.

코드가 프로젝션된 활동에서 실행 중인 경우 프로젝션된 컨텍스트 가져오기

앱의 코드가 프로젝션된 활동 내에서 실행되는 경우 자체 활동 컨텍스트는 이미 프로젝션된 컨텍스트입니다. 이 시나리오에서는 해당 활동 내에서 이루어진 호출이 이미 안경의 하드웨어에 액세스할 수 있습니다.

전화 앱 구성요소에서 실행되는 코드의 예상 컨텍스트 가져오기

프로젝션된 활동 외부의 앱 부분 (예: 휴대전화 활동 또는 서비스)이 안경의 하드웨어에 액세스해야 하는 경우 프로젝션된 컨텍스트를 명시적으로 가져와야 합니다. 이렇게 하려면 createProjectedDeviceContext 메서드를 사용합니다.

@RequiresApi(Build.VERSION_CODES.BAKLAVA)
@OptIn(ExperimentalProjectedApi::class, ExperimentalCoroutinesApi::class)
private fun monitorProjectedConnectivity(activity: ComponentActivity) {
    activity.lifecycleScope.launch {
        // Before creating a projected context, check to see if the projected device is connected.
        // While this method returns true, the projected context remains valid.
        ProjectedContext.isProjectedDeviceConnected(activity, coroutineContext)
            .collectLatest { isConnected ->
                if (isConnected) {
                    // From a phone Activity or Service, get a context for the audio and display glasses.
                    // Re-initialize on reconnect: Obtain another context instance.
                    val projectedContext = try {
                        ProjectedContext.createProjectedDeviceContext(activity)
                    } catch (e: IllegalStateException) {
                        Log.e(TAG, "Failed to create projected context", e)
                        return@collectLatest
                    }

                    // Use the projectedContext to initialize system services (e.g., CameraManager).
                    Log.i(TAG, "Projected device connected. Initializing hardware...")
                } else {
                    // The projected context is destroyed when the device disconnects.
                    // Clean up on disconnect: Listen for 'false' and release resources.
                    Log.i(TAG, "Projected device disconnected. Cleaning up hardware resources...")
                }
            }
    }
}

유효성 확인

ProjectedContext.isProjectedDeviceConnected 내에서 createProjectedDeviceContext 호출을 래핑합니다. 이 메서드는 true를 반환하지만 예측된 컨텍스트는 연결된 기기에 유효하게 유지되며 휴대전화 앱 활동 또는 서비스 (예: CameraManager)는 AI 글라스 하드웨어에 액세스할 수 있습니다.

연결 해제 시 정리

프로젝션된 컨텍스트는 연결된 기기의 수명 주기에 연결되므로 기기 연결이 끊어지면 소멸됩니다. 기기가 연결 해제되면 ProjectedContext.isProjectedDeviceConnected는 false를 반환합니다. 앱은 이 변경사항을 수신 대기하고 앱이 투사된 컨텍스트를 사용하여 만든 시스템 서비스 (예: CameraManager) 또는 리소스를 정리해야 합니다.

다시 연결 시 다시 초기화

글라스가 다시 연결되면 앱은 createProjectedDeviceContext를 사용하여 다른 프로젝션된 컨텍스트 인스턴스를 가져온 다음 새 프로젝션된 컨텍스트를 사용하여 시스템 서비스 또는 리소스를 다시 초기화할 수 있습니다.

글라스의 마이크로 오디오 녹음

블루투스 핸즈프리 프로필 (HFP)을 사용하여 스마트 안경에서 오디오를 녹음할 수 있습니다. 이 모드에서 스마트 안경은 표준 헤드셋 및 고급 오디오 배포 프로필 (A2DP) 프로필을 사용하여 휴대전화에 연결되며 일반적인 블루투스 주변기기처럼 작동합니다. 앱이 이미 표준 블루투스 녹음을 위해 설계된 경우 이 메서드를 사용하여 추가 개발 없이 스마트 안경에서 오디오를 녹음할 수 있습니다.

블루투스 HFP를 사용하여 오디오 녹음

블루투스 HFP를 사용하여 오디오를 녹음하려면 먼저 필요한 런타임 권한을 요청한 다음, 다음 섹션에 설명된 대로 AudioManager API를 사용하여 오디오를 녹음합니다.

권한 요청

표준 블루투스 오디오 기기와 마찬가지로 RECORD_AUDIO, BLUETOOTH_CONNECT, 기타 관련 권한은 연결된 기기 (예: 오디오 글라스 또는 디스플레이 글라스)가 아닌 휴대전화에서 제어합니다.

권한을 요청하려면 다음 단계를 따르세요.

  1. 앱의 매니페스트 파일에서 다음 권한을 선언합니다.

  2. 표준 Android 권한 흐름을 사용하여 런타임에 RECORD_AUDIO 및 BLUETOOTH_CONNECT 권한을 모두 요청합니다.

AudioManager를 사용하여 오디오 라우팅

사용자가 앱에 필요한 런타임 권한을 부여한 후 AudioManager API를 사용하여 통신 기기를 TYPE_BLUETOOTH_SCO로 설정하여 블루투스 HFP를 통해 오디오를 라우팅합니다. 이렇게 하면 시스템이 블루투스 주변기기에서 오디오를 가져옵니다.

val audioManager = context.getSystemService(AudioManager::class.java) ?: return
val devices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)
val hfpDevice = devices.find { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO }

hfpDevice?.let { device ->
    val audioRecord = AudioRecord.Builder()
        .setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
        .setAudioFormat(audioFormat)
        .setBufferSizeInBytes(bufferSize)
        .build()

    // Route recording to the Bluetooth device
    audioRecord.setPreferredDevice(device)
    audioManager.setCommunicationDevice(device)

    audioRecord.startRecording()

안경 카메라로 이미지 캡처

글라스의 카메라로 이미지를 캡처하려면 앱에 적합한 컨텍스트를 사용하여 CameraX의 ImageCapture 사용 사례를 설정하고 글라스의 카메라에 바인딩합니다.

private fun startCameraOnGlasses(activity: ComponentActivity) {
    activity.lifecycleScope.launch {
        // Before creating a projected context, check to see if the projected device is connected.
        ProjectedContext.isProjectedDeviceConnected(activity, coroutineContext)
            .collectLatest { isConnected ->
                if (isConnected) {
                    // 1. Get the CameraProvider using the projected context.
                    // When using the projected context, DEFAULT_BACK_CAMERA maps to the audio and display glasses' camera.
                    val projectedContext = try {
                        ProjectedContext.createProjectedDeviceContext(activity)
                    } catch (e: IllegalStateException) {
                        Log.e(TAG, "Projected context could not be created", e)
                        return@collectLatest
                    }

                    val cameraProviderFuture = ProcessCameraProvider.getInstance(projectedContext)

                    cameraProviderFuture.addListener({
                        val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
                        val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

                        // 2. Check for the presence of a camera.
                        if (!cameraProvider.hasCamera(cameraSelector)) {
                            Log.w(TAG, "The selected camera is not available.")
                            return@addListener
                        }

                        // 3. Query supported streaming resolutions using Camera2 Interop.
                        val cameraInfo = cameraProvider.getCameraInfo(cameraSelector)
                        val camera2CameraInfo = Camera2CameraInfo.from(cameraInfo)
                        val cameraCharacteristics = camera2CameraInfo.getCameraCharacteristic(
                            CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
                        )

                        // 4. Define the resolution strategy.
                        val targetResolution = Size(1920, 1080)
                        val resolutionStrategy = ResolutionStrategy(
                            targetResolution,
                            ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER
                        )
                        val resolutionSelector = ResolutionSelector.Builder()
                            .setResolutionStrategy(resolutionStrategy)
                            .build()

                        // 5. If you have other continuous use cases bound, such as Preview or ImageAnalysis,
                        // you can use  Camera2 Interop's CaptureRequestOptions to set the FPS
                        val fpsRange = Range(30, 60)
                        val captureRequestOptions = CaptureRequestOptions.Builder()
                            .setCaptureRequestOption(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange)
                            .build()

                        // 6. Initialize the ImageCapture use case with options.
                        val imageCapture = ImageCapture.Builder()
                            // Optional: Configure resolution, format, etc.
                            .setResolutionSelector(resolutionSelector)
                            .build()

                        try {
                            // Unbind use cases before rebinding.
                            cameraProvider.unbindAll()

                            // Bind use cases to camera using the Activity as the LifecycleOwner.
                            cameraProvider.bindToLifecycle(
                                activity,
                                cameraSelector,
                                imageCapture
                            )
                        } catch (exc: Exception) {
                            Log.e(TAG, "Use case binding failed", exc)
                        }
                    }, ContextCompat.getMainExecutor(activity))
                }
            }
    }
}

코드에 관한 핵심 사항

  • 프로젝션된 기기 컨텍스트를 사용하여 ProcessCameraProvider의 인스턴스를 가져옵니다.
  • 프로젝션된 컨텍스트 범위 내에서 글라스의 기본 바깥쪽 카메라가 카메라를 선택할 때 DEFAULT_BACK_CAMERA에 매핑됩니다.
  • 사전 결합 검사는 cameraProvider.hasCamera(cameraSelector)를 사용하여 선택한 카메라가 기기에서 사용 가능한지 확인한 후 진행합니다.
  • Camera2CameraInfo와 함께 Camera2 Interop을 사용하여 기본 CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP를 읽습니다. 이는 지원되는 해상도를 고급 검사하는 데 유용할 수 있습니다.
  • 커스텀 ResolutionSelector는 ImageCapture의 출력 이미지 해상도를 정확하게 제어하도록 빌드됩니다.
  • 커스텀 ResolutionSelector로 구성된 ImageCapture 사용 사례를 만듭니다.
  • ImageCapture 사용 사례를 활동의 수명 주기에 바인딩합니다. 이렇게 하면 활동 상태에 따라 카메라 열기 및 닫기를 자동으로 관리합니다 (예: 활동이 일시중지되면 카메라 중지).
  • 단일 스트림 하드웨어 구성: 안경 카메라 파이프라인은 한 번에 하나의 활성 스트림으로 제한됩니다. ImageCapture가 단독으로 바인딩되면 CameraX는 기본적으로 초점 및 측정을 위해 내부 반복 스트림(MeteringRepeating)을 연결하므로 단일 스트림 파이프라인이 중단됩니다. 미리보기 스트림 없이 ImageCapture 자체를 바인딩하려면 CameraXConfig.Builder에서 setRepeatingStreamForced(false)를 설정하여 강제 반복 스트림을 사용 중지합니다(예: Application 클래스에서 CameraXConfig.Provider를 구현하거나 초기화 전에 ProcessCameraProvider를 구성).

안경의 카메라가 설정되면 CameraX의 ImageCapture 클래스를 사용하여 이미지를 캡처할 수 있습니다. takePicture를 사용하여 이미지를 캡처하는 방법을 알아보려면 CameraX 문서를 참고하세요.

안경 카메라로 동영상 촬영하기

스마트 안경의 카메라로 이미지가 아닌 동영상을 캡처하려면 ImageCapture 구성요소를 상응하는 VideoCapture 구성요소로 바꾸고 캡처 실행 로직을 수정합니다.

주요 변경사항은 다른 사용 사례를 사용하고, 다른 출력 파일을 만들고, 적절한 동영상 녹화 방법을 사용하여 캡처를 시작하는 것입니다. VideoCapture API 및 사용 방법에 관한 자세한 내용은 CameraX의 동영상 캡처 문서를 참고하세요.

다음 표에는 앱의 사용 사례에 따라 권장되는 해상도와 프레임 속도가 나와 있습니다.

사용 사례 해상도 프레임 속도
동영상 커뮤니케이션 1280 x 720 15 FPS
컴퓨터 비전 640 x 480 10 FPS
AI 동영상 스트리밍 640 x 480 1 FPS

예측된 활동에서 휴대전화의 하드웨어에 액세스

예측된 활동은 createHostDeviceContext(context)를 사용하여 호스트 기기 (휴대전화)의 컨텍스트를 가져와 휴대전화의 하드웨어 (예: 카메라 또는 마이크)에 액세스할 수도 있습니다.

@OptIn(ExperimentalProjectedApi::class)
private fun getPhoneContext(activity: ComponentActivity): Context? {
    return try {
        // From a projected Activity, get a context for the phone.
        ProjectedContext.createHostDeviceContext(activity)
    } catch (e: IllegalStateException) {
        Log.e(TAG, "Failed to create host device context", e)
        null
    }
}

하이브리드 앱(모바일 및 안경 환경을 모두 포함하는 앱)에서 호스트 기기 (휴대전화)에만 있는 하드웨어나 리소스에 액세스할 때는 올바른 컨텍스트를 명시적으로 선택하여 앱이 올바른 하드웨어에 액세스할 수 있도록 해야 합니다.

  • 휴대전화 Activity의 Activity 컨텍스트 또는 ProjectedContext.createHostDeviceContext를 사용하여 휴대전화의 컨텍스트를 가져옵니다.
  • getApplicationContext를 사용하지 마세요. 투사된 활동이 가장 최근에 실행된 구성요소인 경우 애플리케이션 컨텍스트가 안경의 컨텍스트를 잘못 반환할 수 있기 때문입니다.