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

적용 가능한 XR 기기
이 가이드에서는 이러한 유형의 XR 기기를 위한 환경을 빌드하는 방법을 설명합니다.
오디오 및
디스플레이 안경

필요한 권한을 요청하고 부여받은 후에는 앱이 오디오 글라스 또는 디스플레이 글라스의 하드웨어에 액세스할 수 있습니다. 안경의 하드웨어 (휴대전화의 하드웨어가 아닌)에 액세스하는 방법은 투영된 컨텍스트를 사용하는 것입니다.

코드가 실행되는 위치에 따라 투영된 컨텍스트를 가져오는 방법에는 두 가지가 있습니다.

코드가 투영된 활동에서 실행되는 경우 투영된 컨텍스트 가져오기

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

휴대전화 앱 구성요소에서 실행되는 코드의 투영된 컨텍스트 가져오기

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

@OptIn(ExperimentalProjectedApi::class)
private fun getGlassesContext(context: Context): Context? {
    return try {
        // From a phone Activity or Service, get a context for the AI glasses.
        ProjectedContext.createProjectedDeviceContext(context)
    } catch (e: IllegalStateException) {
        Log.e(TAG, "Failed to create projected device context", e)
        null
    }
}

유효성 확인

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

연결 해제 시 정리

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

다시 연결 시 다시 초기화

안경이 다시 연결되면 앱은 createProjectedDeviceContext를 사용하여 다른 투영된 컨텍스트 인스턴스를 가져온 다음 새 투영된 컨텍스트를 사용하여 시스템 서비스 또는 리소스를 다시 초기화할 수 있습니다.

안경의 마이크로 오디오 녹음

두 가지 고유한 메서드를 사용하여 안경에서 오디오를 녹음할 수 있습니다.

녹음 방법 선택

선택하는 메서드는 고음질의 XR 전용 오디오 처리 또는 표준 블루투스 오디오 입력이 필요한지에 따라 다릅니다.

녹음 방법 마이크 액세스 일반적인 사용 사례

투영된 컨텍스트

여러 마이크

투영된 컨텍스트를 사용하여 녹음하면 앱이 글라스의 여러 마이크와 다음과 같은 특수 하드웨어 기능에 액세스할 수 있습니다.

  • XR 전용 공간화
  • 고급 노이즈 제거
  • 착용자와 주변인의 음성을 구분하는 음성 분리
  • 안경이 활성 블루투스 기기가 아니더라도 다중 기기 환경에서 녹음 액세스 유지

블루투스 HFP

단일 마이크

즉시 사용 가능한 호환성을 위해 블루투스 핸즈프리 프로필 (HFP)을 사용합니다. 이 모드에서 안경은 표준 헤드셋 및 고급 오디오 전송 프로필 (A2DP) 프로필을 사용하여 휴대전화에 연결되며 일반적인 블루투스 주변기기처럼 작동합니다.

앱이 이미 표준 블루투스 녹음을 위해 설계된 경우 이 메서드를 사용하여 XR 전용 기능을 통합하지 않고도 안경에서 오디오를 녹음할 수 있습니다.

투영된 컨텍스트를 사용하여 오디오 녹음

투영된 컨텍스트를 사용하여 오디오를 녹음하려면 먼저 필요한 런타임 권한을 요청한 다음 다음 섹션에 설명된 대로 AudioRecord API를 사용하여 오디오를 녹음합니다.

런타임 권한 요청

안경의 여러 마이크에 액세스하려면 투영된 기기에 오디오 권한을 구체적으로 요청해야 합니다. 사용자가 모바일 기기에서 앱에 부여한 표준 휴대전화 범위의 RECORD_AUDIO 권한은 충분하지 않습니다.

다음 단계에 따라 권한을 요청하세요.

  1. 앱의 매니페스트 파일에서 RECORD_AUDIO 권한을 선언합니다.
  2. 코드가 실행되는 위치에 따라 다음 방법 중 하나로 투영된 기기 범위의 권한을 요청합니다.

투영된 컨텍스트로 AudioRecord 초기화

호스트 휴대전화가 아닌 안경에서 오디오가 녹음되도록 하려면 AudioRecord 객체를 투영된 기기 컨텍스트와 연결해야 합니다.

다음 코드는 AudioRecord.Builder를 사용하고 projectedDeviceContextsetContext 메서드에 전달합니다.

// Initialize AudioRecord with projected device context
val audioRecord = AudioRecord.Builder()
    .setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
    .setAudioFormat(audioFormat)
    .setBufferSizeInBytes(bufferSize)
    // pass in the projected device context
    .setContext(projectedDeviceContext)
    .build()

audioRecord.startRecording()

코드에 관한 주요사항
  • 오디오 소스를 CAMCORDER, VOICE_RECOGNITION, VOICE_COMMUNICATION, 또는 UNPROCESSED로 설정하여 특정 사용 사례에 맞게 오디오 처리를 맞춤설정할 수 있습니다.

    예를 들어 사용 사례에 자동 노이즈 감소가 필요한 경우 VOICE_COMMUNICATION을 사용합니다. VOICE_RECOGNITION 은 AEC (Acoustic Echo Canceler)로 처리됩니다. 그리고 원시의 변경되지 않은 오디오가 필요한 경우 UNPROCESSED 또는 CAMCORDER를 선택합니다.

  • 안경과의 호환성을 보장하려면 audioFormat 객체에서 16kHz의 샘플링 레이트와 모노 또는 스테레오 (CHANNEL_IN_MONO 또는 CHANNEL_IN_STEREO)의 채널 구성을 정의해야 합니다.

  • 버퍼 크기에 관한 고정된 요구사항은 없지만 최소 버퍼 크기를 가져와 인지된 지연 시간을 최소화합니다.

사용 후 정리

앱에 더 이상 마이크가 필요하지 않거나 활동이 중지되면 stoprelease 객체에서 AudioRecord를 호출합니다.

녹음 전에 런타임 권한 확인

startRecording을 호출하기 전에 사용자가 투영된 컨텍스트를 사용하여 안경에 마이크 사용 권한을 부여했는지확인합니다.

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

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

권한 요청

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

다음 단계에 따라 권한을 요청하세요.

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

  2. 표준 Android 권한 흐름을 사용하여 런타임에 RECORD_AUDIOBLUETOOTH_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) {
    // 1. Get the CameraProvider using the projected context.
    // When using the projected context, DEFAULT_BACK_CAMERA maps to the AI glasses' camera.
    val projectedContext = try {
        ProjectedContext.createProjectedDeviceContext(activity)
    } catch (e: IllegalStateException) {
        Log.e(TAG, "AI Glasses context could not be created", e)
        return
    }

    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을 읽습니다.
  • 맞춤 ResolutionSelectorImageCapture의 출력 이미지 해상도를 정확하게 제어하기 위해 빌드됩니다.
  • 맞춤 ResolutionSelector로 구성된 ImageCapture 사용 사례를 만듭니다.
  • ImageCapture 사용 사례를 활동의 수명 주기에 결합합니다. 이렇게 하면 활동의 상태에 따라 카메라 열기 및 닫기가 자동으로 관리됩니다 (예: 활동이 일시중지될 때 카메라 중지).

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

안경의 카메라로 동영상 캡처

안경의 카메라로 이미지가 아닌 동영상을 캡처하려면 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 an AI glasses Activity, get a context for the phone.
        ProjectedContext.createHostDeviceContext(activity)
    } catch (e: IllegalStateException) {
        Log.e(TAG, "Failed to create host device context", e)
        null
    }
}

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

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