Androidカメラアプリ開発の実践ガイド:Camera2 APIによる制御と実装

Camera APIの進化とパイプラインアーキテクチャ

Androidプラットフォームにおけるカメラ機能の実装は、単なる画像の取得に留まらず、ハードウェアリソースの効率的な配分、リアルタイム処理パイプラインの構築、そしてUIスレッドとの同期管理までを総合的に扱わなければなりません。従来のCamera1 APIは利用の手間が少ない一方、並列ストリーム処理の制限や微細な露出・フォーカス制御の欠如から、現代の高度なメディアアプリ開発には限界がありました。Android 5.0(APIレベル21)で導入されたCamera2 APIは、ストリームルーティングモデルと非同期リクエスト処理を採用することで、ハードウェアレベルに近い制御能力を提供しています。このAPIでは、開発者がリクエストをビルドし、それをカメラハードウェアに送信する「リクエスト-レスポンス」モデルが採用されており、複数の出力ターゲットへの同時ストリーミングや、マニュアル撮影モードのサポートが標準化されています。

Camera2 APIの初期化フローとデバイス制御

Camera2 APIの利用開始は、システムサービスからのマネージャーインスタンス取得と、対象レンズの特性取得から始まります。カメラデバイスのオープン操作は非同期で実行されるため、ステートコールバックを用いて接続完了やエラー状態を適切にハンドリングすることが必須です。以下は、カメラデバイスの初期化とセッション構築準備までの標準的な実装フローです。

CameraManager camManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
String lensId = camManager.getCameraIdList()[0];
CameraCharacteristics sensorProfile = camManager.getCameraCharacteristics(lensId);

CameraDevice.StateCallback deviceListener = new CameraDevice.StateCallback() {
    @Override
    public void onOpened(CameraDevice activeDevice) {
        // デバイス接続成功後にセッション構築メソッドへ遷移
        initializeCaptureSession(activeDevice);
    }

    @Override
    public void onDisconnected(CameraDevice activeDevice) {
        activeDevice.close();
        // 外部切断時のUI更新ロジック
    }

    @Override
    public void onError(CameraDevice activeDevice, int errorType) {
        activeDevice.close();
        // ハードウェアレベルのエラーコードに基づくフォールバック処理
    }
};

camManager.openCamera(lensId, deviceListener, mainExecutor);

プレビューレンダリングの構築とUI統合

カメラデータの可視化には、SurfaceViewTextureViewの2つのコンポーネントが利用可能です。SurfaceViewは独立したウィンドウ層でレンダリングされるため、高いフレームレートを維持しやすい反面、ウィンドウ階層の重なりや変換アニメーションには不向きです。一方、TextureViewはビュー階層内で処理されるため、透明背景やスケーリング、回転などのビュー変換と親和性が高いですが、GPU合成によるオーバーヘッドを考慮する必要があります。アプリケーションのUX要件に応じて適切な選択を行い、ライフサイクル( onResume/onPause )と連動してカメラの接続・切断を管理することがパフォーマンス安定の鍵となります。

private Size resolveOptimalPreviewSize(StreamConfigurationMap configMap, SurfaceTexture renderTarget) {
    Size[] supportedDimensions = configMap.getOutputSizes(SurfaceTexture.class);
    // 縦横比を維持しつつ、ターゲットビューの解像度に最も近いサイズを検出
    return Arrays.stream(supportedDimensions)
            .filter(dim -> dim.getWidth() >= dim.getHeight())
            .sorted((a, b) -> Long.compare((long) b.getWidth() * b.getHeight(), (long) a.getWidth() * a.getHeight()))
            .findFirst()
            .orElse(supportedDimensions[0]);
}

静止画キャプチャと動画エンコーディングの実装

撮影機能の実装は、CaptureRequestのビルドと出力ターゲットの指定で行われます。静止画取得時は通常、ImageReaderを介してJPEGまたはRAWデータを取得し、ファイルシステムへ書き込みます。動画録画については、MediaRecorderをラップする方法もありますが、高度な制御やカスタムエンコードが必要な場合はMediaCodecを直接使用し、インプットサーフェスへカメラストリームを転送する構成が採用されます。

// 静止画リクエストの構築
CaptureRequest.Builder stillRequestBuilder = activeDevice.createCaptureRequest(CameraDevice.TemplateType.TEMPLATE_STILL_CAPTURE);
stillRequestBuilder.addTarget(imageSink.getSurface());
stillRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
stillRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);

captureSession.capture(stillRequestBuilder.build(), captureCallback, mainExecutor);

// 動画エンコーダーの初期化とバッファ処理スレッド
MediaCodec videoEncoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
MediaFormat encFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, targetWidth, targetHeight);
encFormat.setInteger(MediaFormat.KEY_BIT_RATE, 8_000_000);
encFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
encFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
encFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
videoEncoder.configure(encFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);

Surface codecInputSurface = videoEncoder.createInputSurface();
videoEncoder.start();

new Thread(() -> {
    MediaCodec.BufferInfo bufferMeta = new MediaCodec.BufferInfo();
    boolean encodeLoopActive = true;
    while (encodeLoopActive) {
        int outputIndex = videoEncoder.dequeueOutputBuffer(bufferMeta, 10_000);
        if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
            // 出力ストリームのヘッダー更新処理
        } else if (outputIndex >= 0) {
            if ((bufferMeta.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                encodeLoopActive = false;
            }
            videoEncoder.releaseOutputBuffer(outputIndex, false);
        }
    }
    videoEncoder.stop();
    videoEncoder.release();
}).start();

画像や動画データの永続化はI/O密集型処理であるため、メインスレッドをブロックしないようExecutorServiceやコルーチン、AsyncTaskの代替手段を用いてバックグラウンド処理に分離してください。また、Imageオブジェクトの使用後は必ずclose()を呼び出し、ネイティブメモリリークを防止する必要があります。

ランタイム権限のダイナミック管理

Android 6.0以降では、カメラやストレージへのアクセス権限はインストール時ではなく、アプリケーション実行中にユーザーの明示的な承認を得る形式に切り替わりました。権限チェックは機能実行直前に行い、未付与の場合はActivityCompat.requestPermissions()を呼び出します。ユーザーによる拒否が発生した場合、機能の一部を制限するのではなく、代替UIを提供するか、設定画面へ誘導するなどのグレースフルデグラデーション設計がUX向上に寄与します。

if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
    ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, PERMISSION_REQ_CODE);
}

@Override
public void onRequestPermissionsResult(int requestCode, String[] grantedPermissions, int[] grantResults) {
    if (requestCode == PERMISSION_REQ_CODE && grantResults.length > 0) {
        if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            // 権限付与後のカメラ初期化処理
        } else {
            // 拒否時の代替機能表示または教育ダイアログの表示
        }
    }
}

ハードウェア特性に応じた動的適応戦略

Android端末のカメラ構成は多様であり、フラッグシップ機からエントリーモデルまでハードウェアサポートレベルが大きく異なります。CameraCharacteristicsクラスを適切に活用することで、実行時に端末が提供するキャパシティを調べ、アプリケーションの動作モードを動的に切り替えることができます。特にINFO_SUPPORTED_HARDWARE_LEVELは、マニュアル制御や高頻度ストリーミングが可能かどうかを判定する重要な指標となります。

Integer hardwareTier = characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
switch (hardwareTier) {
    case CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL:
        // FULL: 高度なAE/AF制御、マルチストリーム同時出力が保証
        enableManualExposureControl();
        break;
    case CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED:
    case CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY:
        // LIMITED/LEGACY: 基本的なオートモードに制限し、処理パイプラインを最適化
        fallbackToStandardAutoMode();
        break;
}

// センサー配列サイズを取得し、ノイズ低減アルゴリズムのパラメータを調整
Size pixelArray = characteristics.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE);
if (pixelArray != null) {
    configureNoiseReductionThreshold(pixelArray.getWidth(), pixelArray.getHeight());
}

また、レンズの向き(LENS_FACING)や、利用可能なフォーカスモード(CONTROL_AF_AVAILABLE_MODES)を照会することで、UIレイアウトのミラーリング処理や、タッチフォーカス対応可否を端末ごとに判定できます。ハードウェア特性を事前にキャッシュし、セッション構築時に適切なCaptureRequestパラメータを注入することで、端末依存のクラッシュを防ぎ、一貫した撮影品質を保証することが可能です。

タグ: Android camera2-api Java mediacodec android-permissions

6月24日 01:44 投稿