投影されたコンテキストを使用して、オーディオ グラスとディスプレイ グラスのハードウェアにアクセスする

対象となる XR デバイス
このガイダンスは、次のようなタイプの XR デバイス向けのエクスペリエンスを構築する際に役立ちます。
オーディオ&
ディスプレイ グラス

必要な権限をリクエストして付与されると、アプリはオーディオ グラスまたはディスプレイ グラスのハードウェアにアクセスできるようになります。(スマートフォンのハードウェアではなく)グラスのハードウェアにアクセスするための鍵は、投影されたコンテキストを使用することです。

投影されたコンテキストを取得するには、コードの実行場所に応じて主に次の 2 つの方法があります。

コードが投影されたアクティビティで実行されている場合は、投影されたコンテキストを取得する

アプリのコードが投影されたアクティビティ内から実行されている場合、そのアクティビティ コンテキストはすでに投影されたコンテキストになっています。このシナリオでは、そのアクティビティ内で行われた呼び出しは、すでにグラスのハードウェアにアクセスできます。

電話アプリ コンポーネントで実行されているコードの投影コンテキストを取得する

投影されたアクティビティの外部にあるアプリの一部(電話アクティビティやサービスなど)がグラスのハードウェアにアクセスする必要がある場合は、投影されたコンテキストを明示的に取得する必要があります。これを行うには、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...")
                }
            }
    }
}

有効性を確認する

createProjectedDeviceContext 呼び出しを ProjectedContext.isProjectedDeviceConnected 内にラップします。このメソッドは true を返しますが、投影されたコンテキストは接続されたデバイスに対して有効なままであり、スマートフォン アプリのアクティビティまたはサービス(CameraManager など)は AI メガネのハードウェアにアクセスできます。

接続解除時にクリーンアップする

投影されたコンテキストは、コネクテッド デバイスのライフサイクルに関連付けられているため、デバイスが切断されると破棄されます。デバイスが切断されると、ProjectedContext.isProjectedDeviceConnected は false を返します。アプリはこの変更をリッスンし、その投影されたコンテキストを使用してアプリが作成したシステム サービス(CameraManager など)やリソースをクリーンアップする必要があります。

再接続時に再初期化する

グラスが再接続されると、アプリは createProjectedDeviceContext を使用して別の投影コンテキスト インスタンスを取得し、新しい投影コンテキストを使用してシステム サービスまたはリソースを再初期化できます。

メガネのマイクで音声を録音する

Bluetooth ハンズフリー プロファイル(HFP)を使用して、グラスから音声を録音できます。このモードでは、グラスは標準のヘッドセット プロファイルと Advanced Audio Distribution Profile(A2DP)プロファイルを使用してスマートフォンに接続し、一般的な Bluetooth 周辺機器のように機能します。アプリがすでに標準の Bluetooth 録音用に設計されている場合は、この方法を使用することで、追加の開発なしでグラスから音声を録音できます。

Bluetooth HFP を使用して音声を録音する

Bluetooth HFP を使用して音声を録音するには、まず必要な実行時の権限をリクエストしてから、AudioManager API を使用して音声を録音します。詳しくは、以下のセクションをご覧ください。

権限をリクエスト

標準の Bluetooth オーディオ デバイスと同様に、RECORD_AUDIO、BLUETOOTH_CONNECT、その他の関連する権限は、接続されたデバイス(オーディオ グラスやディスプレイ グラスなど)ではなく、スマートフォンによって制御されます。

権限をリクエストする手順は次のとおりです。

  1. アプリのマニフェスト ファイルで次の権限を宣言します。

  2. 標準の Android 権限フローを使用して、実行時に RECORD_AUDIO 権限と BLUETOOTH_CONNECT 権限の両方をリクエストします。

AudioManager を使用して音声をルーティングする

ユーザーがアプリに必要な実行時の権限を付与したら、AudioManager API を使用して通信デバイスを TYPE_BLUETOOTH_SCO に設定し、Bluetooth HFP 経由で音声をルーティングします。これにより、Bluetooth ペリフェラルから音声を取得するようシステムに指示します。

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 ユースケースをアクティビティのライフサイクルにバインドします。これにより、アクティビティの状態に基づいてカメラの開閉が自動的に管理されます(たとえば、アクティビティが一時停止されたときにカメラが停止されます)。
  • シングルストリーム ハードウェアの構成: メガネカメラのパイプラインは、一度に 1 つのアクティブなストリームに制限されます。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 または ProjectedContext.createHostDeviceContext から Activity コンテキストを使用して、スマートフォンのコンテキストを取得します。
  • getApplicationContext は使用しないでください。投影されたアクティビティが最後に起動されたコンポーネントであった場合、アプリケーション コンテキストがグラスのコンテキストを誤って返す可能性があるためです。