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

적용 가능한 XR 기기
이 안내는 이러한 유형의 XR 기기용 환경을 구축하는 데 도움이 됩니다.
오디오 및
디스플레이 글래스

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

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

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

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

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

예상 활동 외부의 앱 부분 (예: 전화 활동 또는 서비스)이 글라스의 하드웨어에 액세스해야 하는 경우 예상 컨텍스트를 명시적으로 획득해야 합니다. 이렇게 하려면 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를 사용하여 다른 투영된 컨텍스트 인스턴스를 가져온 다음 새 투영된 컨텍스트를 사용하여 시스템 서비스나 리소스를 다시 초기화할 수 있습니다.

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

다음 두 가지 방법을 사용하여 안경에서 오디오를 녹음할 수 있습니다.

  • 예상 컨텍스트를 사용합니다.
  • 블루투스 핸즈프리 프로필 (HFP)을 사용합니다.

녹화 방법 선택

선택하는 방법은 고품질의 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)로 처리됩니다. 변경되지 않은 원본 오디오가 필요한 경우 UNPROCESSED 또는 CAMCORDER를 선택합니다.

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

  • AudioRecord.getMinBufferSize()를 사용하여 AudioRecord 객체를 만드는 최소 버퍼 크기를 결정합니다. 하지만 안경에서 오디오가 끊기는 것을 방지하려면 전체 버퍼가 채워질 때까지 기다리지 말고 짧고 빈번한 청크(이상적으로는 20ms 슬라이스)로 이 버퍼에서 읽어야 합니다.

사용 후 정리

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

녹화 전에 런타임 권한 확인

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을 읽습니다. 이는 지원되는 해상도에 관한 고급 검사에 유용할 수 있습니다.
  • ImageCapture의 출력 이미지 해상도를 정확하게 제어하기 위해 맞춤 ResolutionSelector가 빌드됩니다.
  • 맞춤 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
    }
}

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

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