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

対応する XR デバイス
このガイダンスは、次のようなタイプの XR デバイス向けのエクスペリエンスを構築する際に役立ちます。
音声と
ディスプレイのメガネ

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

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

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

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

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

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

有効性を確認する

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

切断時にクリーンアップする

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

再接続時に再初期化する

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

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

メガネから音声を録音するには、次の 2 つの方法があります。

録音方法を選択する

選択するメソッドは、高忠実度で XR 固有の音声処理が必要かどうか、または標準の Bluetooth 音声入力が必要かどうかによって異なります。

録音方法 マイクへのアクセス 一般的なユースケース

投影されたコンテキスト

複数のマイク

投影されたコンテキストを使用して録音すると、アプリはメガネの複数のマイクと、次のような専用のハードウェア機能にアクセスできます。

  • XR 固有の空間化。
  • 高度なノイズ除去。
  • 装着者の声と 傍観者の声を区別する音声分離。
  • メガネがアクティブな Bluetooth デバイスでない場合でも、マルチデバイス環境で録音アクセスを維持する。

Bluetooth HFP

単一のマイク

すぐに使用できる互換性については、Bluetooth ハンズフリー プロファイル(HFP)に依存します。このモードでは、メガネは標準のヘッドセット プロファイルと Advanced Audio Distribution Profile(A2DP)プロファイルを使用してスマートフォンに接続し、一般的な Bluetooth 周辺機器のように機能します。

アプリが標準の Bluetooth 録音用に設計されている場合は、この方法を使用して、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()

コードに関する主なポイント
  • 音声ソースを CAMCORDERVOICE_RECOGNITIONVOICE_COMMUNICATION、または UNPROCESSED に設定して、特定のユース ケースに合わせて音声処理を調整できます。

    たとえば、ユースケースで自動ノイズリダクションが必要な場合は、VOICE_COMMUNICATION を使用します。VOICE_RECOGNITION は音響エコー キャンセラ(AEC)で処理されます。未加工の音声が必要な場合は、UNPROCESSED または CAMCORDER を選択します。

  • メガネとの互換性を確保するには、audioFormat オブジェクトで 16 kHz のサンプルレートと、モノラルまたはステレオのチャンネル構成(CHANNEL_IN_MONO または CHANNEL_IN_STEREO を使用)を定義する必要があります。

  • バッファサイズに固定の要件はありませんが、認識されるレイテンシを最小限に抑えるために最小バッファ サイズを取得します。

使用後にクリーンアップする

アプリでマイクが不要になった場合、またはアクティビティが停止した場合は、 AudioRecord オブジェクトで stoprelease を呼び出します。

録音前に実行時の権限を確認する

startRecording を呼び出す前に、投影されたコンテキストを使用して、ユーザーがメガネの マイクの権限を付与していることを確認します

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) {
    // 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) を使用して、選択したカメラがデバイスで使用可能であることを確認してから処理を進めます。
  • Camera2CameraInfoCamera2 Interop を使用して、 基盤となる CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP を読み取ります。 これは、サポートされている解像度の高度なチェックに役立ちます。
  • カスタムの ResolutionSelector は、出力 画像解像度を ImageCapture のために正確に制御するために構築されています。
  • カスタムの 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 は使用しないでください。投影されたアクティビティが最後に起動されたコンポーネントである場合、アプリケーション コンテキスト がメガネのコンテキストを誤って返す可能性があります。