Architecture de CameraX

Cette page présente l'architecture de CameraX, y compris sa structure, et explique comment utiliser l'API, les cycles de vie et comment combiner des cas d'utilisation.

Structure de CameraX

Vous pouvez utiliser CameraX pour faire l'interface avec la caméra d'un appareil via une abstraction appelée "cas d'utilisation". Les cas d'utilisation suivants sont disponibles :

  • Preview (Aperçu) : accepte une surface pour y afficher un aperçu, comme une PreviewView.
  • Image analysis (Analyse des images) : fournit des tampons accessibles par le processeur pour l'analyse, par exemple pour le machine learning.
  • Image capture (Capture d'image) : prend et enregistre une photo.
  • Video capture (Capture vidéo) : capture un contenu vidéo et audio avec VideoCapture.

Les cas d'utilisation peuvent être combinés et actifs en même temps. Par exemple, une application peut permettre à l'utilisateur de visualiser l'image que voit la caméra à l'aide d'un cas d'utilisation "Aperçu", exploiter un cas d'utilisation "Analyse des images" qui détermine si les personnes sur la photo sourient, et inclure un cas d'utilisation "Capture d'image" pour prendre une photo dès qu'elles le font.

Modèle d'API

Pour utiliser la bibliothèque, vous devez spécifier les éléments suivants :

  • Le cas d'utilisation souhaité ainsi que les options de configuration ;
  • Quoi faire des données de sortie en joignant des écouteurs ;
  • Le flux visé, par exemple à quels moments activer les caméras et produire des données, en liant le cas d'utilisation aux cycles de vie des composants de l'architecture Android.

Il existe deux façons d'écrire une application CameraX : CameraController (idéal pour une utilisation la plus simple possible de CameraX) ou CameraProvider (si vous avez besoin de plus de flexibilité).

CameraController

Un CameraController fournit la plupart des fonctionnalités de base de CameraX dans une seule classe. Il nécessite peu de code de configuration et gère automatiquement l'initialisation de l'appareil photo, la gestion des cas d'utilisation, la rotation cible, la mise au point en appuyant sur l'aperçu, le pincement pour zoomer, etc. La classe concrète qui étend CameraController est LifecycleCameraController .

Kotlin

val previewView: PreviewView = viewBinding.previewView
var cameraController = LifecycleCameraController(baseContext)
cameraController.bindToLifecycle(this)
cameraController.cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
previewView.controller = cameraController

Java

PreviewView previewView = viewBinding.previewView;
LifecycleCameraController cameraController = new LifecycleCameraController(baseContext);
cameraController.bindToLifecycle(this);
cameraController.setCameraSelector(CameraSelector.DEFAULT_BACK_CAMERA);
previewView.setController(cameraController);

Les cas d'utilisation (UseCase) par défaut pour CameraController sont Preview, ImageCapture et ImageAnalysis. Pour désactiver ImageCapture ou ImageAnalysis, ou encore pour activer VideoCapture, utilisez la méthode setEnabledUseCases().

Pour découvrir d'autres utilisations de CameraController, consultez l'exemple de lecteur de code QR ou la vidéo présentant les principes de base de CameraController.

CameraProvider

Un CameraProvider reste facile à utiliser, mais étant donné que le développeur de l'application gère une plus grande partie de la configuration, les possibilités de personnalisation sont plus nombreuses. Par exemple, il est possible d'activer la rotation de l'image de sortie ou de définir le format de l'image de sortie dans ImageAnalysis. Vous pouvez également utiliser une Surface personnalisée pour l'aperçu de l'appareil photo pour plus de flexibilité, tandis qu'avec CameraController, vous devez utiliser une PreviewView. L'utilisation du code Surface existant peut être utile s'il s'agit déjà d'une entrée dans d'autres parties de votre application.

Vous configurez les cas d'utilisation à l'aide des méthodes set() et les finalisez avec la méthode build(). Chaque objet de cas d'utilisation fournit un ensemble d'API spécifiques à chaque cas d'utilisation. Par exemple, le cas d'utilisation "Capture d'image" fournit un appel de la méthode takePicture().

Au lieu de placer des appels de méthode spécifiques de démarrage et d'arrêt dans onResume() et onPause(), l'application indique un cycle de vie auquel associer la caméra à l'aide de cameraProvider.bindToLifecycle(). Ce cycle de vie indique ensuite à CameraX à quel moment configurer la session de capture et garantit que l'état de la caméra change de manière appropriée pour coïncider avec ses propres transitions.

Pour connaître les étapes à suivre afin d'implémenter chaque cas d'utilisation, consultez Intégrer un aperçu, Analyser les images, Capture d'image et Capture vidéo.

Le cas d'utilisation "Aperçu" interagit avec une Surface pour l'affichage. Les applications créent le cas d'utilisation avec des options de configuration à l'aide du code suivant :

Kotlin

val preview = Preview.Builder().build()
val viewFinder: PreviewView = findViewById(R.id.previewView)

// The use case is bound to an Android Lifecycle with the following code
val camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview)

// PreviewView creates a surface provider and is the recommended provider
preview.setSurfaceProvider(viewFinder.getSurfaceProvider())

Java

Preview preview = new Preview.Builder().build();
PreviewView viewFinder = findViewById(R.id.view_finder);

// The use case is bound to an Android Lifecycle with the following code
Camera camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview);

// PreviewView creates a surface provider, using a Surface from a different
// kind of view will require you to implement your own surface provider.
preview.previewSurfaceProvider = viewFinder.getSurfaceProvider();

Pour d'autres exemples de code, consultez l'application exemple officielle de CameraX.

Cycles de vie de CameraX

CameraX observe un cycle de vie pour déterminer à quels moments ouvrir la caméra, créer une session de capture et s'arrêter, puis s'éteindre. Les API de cas d'utilisation fournissent des appels de méthode et des rappels pour surveiller la progression.

Comme expliqué dans Combiner les cas d'utilisation, vous pouvez lier des combinaisons de cas d'utilisation à un seul cycle de vie. Si votre application doit prendre en charge des cas d'utilisation qui ne peuvent pas être combinés, vous pouvez effectuer l'une des opérations suivantes :

  • Regrouper des cas d'utilisation compatibles dans plusieurs fragments, puis passer d'un fragment à un autre ;
  • Créer un composant de cycle de vie personnalisé et l'utiliser pour contrôler manuellement le cycle de vie de la caméra.

Si vous dissociez les propriétaires du cycle de vie de vos cas d'utilisation "Vue" et "Caméra" (par exemple, si vous utilisez un cycle de vie personnalisé ou un fragment de conservation), vous devez vous assurer que tous les cas d'utilisation sont déliés de CameraX à l'aide de ProcessCameraProvider.unbindAll() ou de façon individuelle. Autrement, lorsque vous liez des cas d'utilisation à un cycle de vie, vous pouvez aussi laisser CameraX s'occuper d'ouvrir et de fermer la session de capture et de délier les cas d'utilisation.

Imaginons que le fonctionnement tout entier de votre caméra corresponde au cycle de vie d'un seul composant sensible au cycle de vie, tel qu'un fragment AppCompatActivity ou AppCompat. Dans ce cas, le fait d'utiliser le cycle de vie de ce composant au moment de lier tous les cas d'utilisation souhaités garantira que la caméra sera prête à fonctionner lorsque le composant sensible au cycle de vie sera actif, et qu'elle sera sinon supprimée de manière sécurisée pour ne pas consommer de ressources.

LifecycleOwners personnalisés

Pour les cas avancés, vous pouvez créer un LifecycleOwner personnalisé pour permettre à votre application de contrôler de manière explicite le cycle de vie de la session CameraX au lieu de l'associer à un LifecycleOwner Android standard.

L'exemple de code suivant illustre comment créer un LifecycleOwner personnalisé simple :

Kotlin

class CustomLifecycle : LifecycleOwner {
    private val lifecycleRegistry: LifecycleRegistry

    init {
        lifecycleRegistry = LifecycleRegistry(this);
        lifecycleRegistry.markState(Lifecycle.State.CREATED)
    }
    ...
    fun doOnResume() {
        lifecycleRegistry.markState(State.RESUMED)
    }
    ...
    override fun getLifecycle(): Lifecycle {
        return lifecycleRegistry
    }
}

Java

public class CustomLifecycle implements LifecycleOwner {
    private LifecycleRegistry lifecycleRegistry;
    public CustomLifecycle() {
        lifecycleRegistry = new LifecycleRegistry(this);
        lifecycleRegistry.markState(Lifecycle.State.CREATED);
    }
   ...
   public void doOnResume() {
        lifecycleRegistry.markState(State.RESUMED);
    }
   ...
    public Lifecycle getLifecycle() {
        return lifecycleRegistry;
    }
}

À l'aide de ce LifecycleOwner, votre application peut placer des transitions d'état aux points souhaités dans son code. Pour savoir comment intégrer ce fonctionnement dans votre application, consultez Intégrer un LifecycleOwner personnalisé.

Cas d'utilisation simultanés

Les cas d'utilisation peuvent être exécutés simultanément. Bien que les cas d'utilisation puissent être liés de manière séquentielle à un cycle de vie, il est préférable de tous les lier à l'aide d'un seul appel à CameraProcessProvider.bindToLifecycle(). Pour en savoir plus sur les bonnes pratiques concernant les modifications de configuration, consultez Gérer les modifications de configuration.

Dans l'exemple de code suivant, l'application précise les deux cas d'utilisation à créer et à exécuter simultanément. Elle précise aussi le cycle de vie à utiliser pour les deux cas d'utilisation, de sorte qu'ils démarrent et s'arrêtent en fonction du cycle de vie.

Kotlin

private lateinit var imageCapture: ImageCapture

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

    cameraProviderFuture.addListener(Runnable {
        // Camera provider is now guaranteed to be available
        val cameraProvider = cameraProviderFuture.get()

        // Set up the preview use case to display camera preview.
        val preview = Preview.Builder().build()

        // Set up the capture use case to allow users to take photos.
        imageCapture = ImageCapture.Builder()
                .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
                .build()

        // Choose the camera by requiring a lens facing
        val cameraSelector = CameraSelector.Builder()
                .requireLensFacing(CameraSelector.LENS_FACING_FRONT)
                .build()

        // Attach use cases to the camera with the same lifecycle owner
        val camera = cameraProvider.bindToLifecycle(
                this as LifecycleOwner, cameraSelector, preview, imageCapture)

        // Connect the preview use case to the previewView
        preview.setSurfaceProvider(
                previewView.getSurfaceProvider())
    }, ContextCompat.getMainExecutor(this))
}

Java

private ImageCapture imageCapture;

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    PreviewView previewView = findViewById(R.id.previewView);

    ListenableFuture<ProcessCameraProvider> cameraProviderFuture =
            ProcessCameraProvider.getInstance(this);

    cameraProviderFuture.addListener(() -> {
        try {
            // Camera provider is now guaranteed to be available
            ProcessCameraProvider cameraProvider = cameraProviderFuture.get();

            // Set up the view finder use case to display camera preview
            Preview preview = new Preview.Builder().build();

            // Set up the capture use case to allow users to take photos
            imageCapture = new ImageCapture.Builder()
                    .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
                    .build();

            // Choose the camera by requiring a lens facing
            CameraSelector cameraSelector = new CameraSelector.Builder()
                    .requireLensFacing(lensFacing)
                    .build();

            // Attach use cases to the camera with the same lifecycle owner
            Camera camera = cameraProvider.bindToLifecycle(
                    ((LifecycleOwner) this),
                    cameraSelector,
                    preview,
                    imageCapture);

            // Connect the preview use case to the previewView
            preview.setSurfaceProvider(
                    previewView.getSurfaceProvider());
        } catch (InterruptedException | ExecutionException e) {
            // Currently no exceptions thrown. cameraProviderFuture.get()
            // shouldn't block since the listener is being called, so no need to
            // handle InterruptedException.
        }
    }, ContextCompat.getMainExecutor(this));
}

Les combinaisons de configuration suivantes sont compatibles (lorsque la prévisualisation ou la capture vidéo sont requises, mais pas en même temps) :

Aperçu ou capture vidéo Capture d'image Analyse Descriptions
Proposer un aperçu ou enregistrer une vidéo, prendre une photo et analyser le flux d'images.
  Prendre une photo et analyser le flux d'images.
  Proposer un aperçu ou enregistrer une vidéo, et prendre une photo.
  Proposer un aperçu ou enregistrer une vidéo, et analyser le flux d'images.

Lorsque l'aperçu et la capture vidéo sont nécessaires, les combinaisons de cas d'utilisation suivantes sont compatibles sous conditions :

Aperçu Capture vidéo Capture d'image Analyse Exigence particulière
    Garanti pour toutes les caméras
  Caméra LIMITED (ou mieux).
  Caméra LEVEL_3 (ou mieux).

De plus :

  • Chaque cas d'utilisation peut fonctionner seul. Par exemple, une application peut enregistrer une vidéo sans utiliser l'aperçu.
  • Lorsque les extensions sont activées, seule la combinaison de ImageCapture et Preview est assurée de fonctionner. En fonction de l'intégration OEM, il n'est pas toujours possible d'ajouter ImageAnalysis. Les extensions ne peuvent être activées pour le cas d'utilisation VideoCapture. Pour en savoir plus, consultez les documents de référence concernant les extensions.
  • Suivant leur capacité, il est possible que certaines caméras acceptent la combinaison dans des modes de résolution inférieure, mais ne puissent pas l'accepter pour des résolutions plus élevées.

Le niveau de matériel compatible peut être récupéré à partir de Camera2CameraInfo. Par exemple, le code suivant vérifie si la caméra arrière par défaut est un appareil LEVEL_3 :

Kotlin

@androidx.annotation.OptIn(ExperimentalCamera2Interop::class)
fun isBackCameraLevel3Device(cameraProvider: ProcessCameraProvider) : Boolean {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        return CameraSelector.DEFAULT_BACK_CAMERA
            .filter(cameraProvider.availableCameraInfos)
            .firstOrNull()
            ?.let { Camera2CameraInfo.from(it) }
            ?.getCameraCharacteristic(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL) ==
            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3
    }
    return false
}

Java

@androidx.annotation.OptIn(markerClass = ExperimentalCamera2Interop.class)
Boolean isBackCameraLevel3Device(ProcessCameraProvider cameraProvider) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        List\ filteredCameraInfos = CameraSelector.DEFAULT_BACK_CAMERA
                .filter(cameraProvider.getAvailableCameraInfos());
        if (!filteredCameraInfos.isEmpty()) {
            return Objects.equals(
                Camera2CameraInfo.from(filteredCameraInfos.get(0)).getCameraCharacteristic(
                        CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL),
                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3);
        }
    }
    return false;
}

Autorisations

Votre appli aura besoin de l'autorisation de CAMERA. Pour enregistrer des images dans des fichiers, vous devez également disposer de l'autorisation WRITE_EXTERNAL_STORAGE, sauf sur les appareils équipés d'Android version 10 ou ultérieure.

Pour en savoir plus sur la configuration des autorisations pour votre application, consultez Demander des autorisations pour une application.

Exigences

Les exigences minimales de version de CameraX sont les suivantes :

  • Niveau 21 d'API Android
  • Composants d'architecture Android 1.1.1

Pour les activités sensibles au cycle de vie, utilisez FragmentActivity ou AppCompatActivity.

Déclarer des dépendances

Pour ajouter une dépendance à CameraX, vous devez ajouter le dépôt Maven de Google à votre projet.

Ouvrez le fichier settings.gradle de votre projet et ajoutez le dépôt google() comme illustré ci-dessous :

Groovy

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}

Kotlin

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}

Ajoutez le code suivant à la fin du bloc Android :

Groovy

android {
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    // For Kotlin projects
    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Kotlin

android {
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    // For Kotlin projects
    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Ajoutez le code suivant au fichier build.gradle de chaque module pour une appli :

Groovy

dependencies {
  // CameraX core library using the camera2 implementation
  def camerax_version = "1.4.0-alpha04"
  // The following line is optional, as the core library is included indirectly by camera-camera2
  implementation "androidx.camera:camera-core:${camerax_version}"
  implementation "androidx.camera:camera-camera2:${camerax_version}"
  // If you want to additionally use the CameraX Lifecycle library
  implementation "androidx.camera:camera-lifecycle:${camerax_version}"
  // If you want to additionally use the CameraX VideoCapture library
  implementation "androidx.camera:camera-video:${camerax_version}"
  // If you want to additionally use the CameraX View class
  implementation "androidx.camera:camera-view:${camerax_version}"
  // If you want to additionally add CameraX ML Kit Vision Integration
  implementation "androidx.camera:camera-mlkit-vision:${camerax_version}"
  // If you want to additionally use the CameraX Extensions library
  implementation "androidx.camera:camera-extensions:${camerax_version}"
}

Kotlin

dependencies {
    // CameraX core library using the camera2 implementation
    val camerax_version = "1.4.0-alpha04"
    // The following line is optional, as the core library is included indirectly by camera-camera2
    implementation("androidx.camera:camera-core:${camerax_version}")
    implementation("androidx.camera:camera-camera2:${camerax_version}")
    // If you want to additionally use the CameraX Lifecycle library
    implementation("androidx.camera:camera-lifecycle:${camerax_version}")
    // If you want to additionally use the CameraX VideoCapture library
    implementation("androidx.camera:camera-video:${camerax_version}")
    // If you want to additionally use the CameraX View class
    implementation("androidx.camera:camera-view:${camerax_version}")
    // If you want to additionally add CameraX ML Kit Vision Integration
    implementation("androidx.camera:camera-mlkit-vision:${camerax_version}")
    // If you want to additionally use the CameraX Extensions library
    implementation("androidx.camera:camera-extensions:${camerax_version}")
}

Pour savoir comment configurer votre appli afin de répondre à ces exigences, consultez Déclarer des dépendances.

Interopérabilité de CameraX avec Camera2

CameraX est basé sur Camera2, et CameraX propose des moyens de lire et même d'écrire des propriétés dans l'implémentation de Camera2. Pour en savoir plus, consultez le package d'interopérabilité.

Pour en savoir plus sur la configuration des propriétés de Camera2 par CameraX, utilisez Camera2CameraInfo afin de lire les CameraCharacteristics sous-jacentes. Vous pouvez également choisir d'écrire les propriétés sous-jacentes de Camera2 de deux manières :

L'exemple de code suivant utilise des cas d'utilisation de flux pour optimiser un appel vidéo. Utilisez Camera2CameraInfo pour vérifier si le cas d'utilisation du flux d'appel vidéo est disponible. Utilisez ensuite un Camera2Interop.Extender pour définir le cas d'utilisation du flux sous-jacent.

Kotlin

// Set underlying Camera2 stream use case to optimize for video calls.

val videoCallStreamId =
    CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_CALL.toLong()

// Check available CameraInfos to find the first one that supports
// the video call stream use case.
val frontCameraInfo = cameraProvider.getAvailableCameraInfos()
    .first { cameraInfo ->
        val isVideoCallStreamingSupported = Camera2CameraInfo.from(cameraInfo)
            .getCameraCharacteristic(
                CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES
            )?.contains(videoCallStreamId)
        val isFrontFacing = (cameraInfo.getLensFacing() == 
                             CameraSelector.LENS_FACING_FRONT)
        (isVideoCallStreamingSupported == true) && isFrontFacing
    }

val cameraSelector = frontCameraInfo.cameraSelector

// Start with a Preview Builder.
val previewBuilder = Preview.Builder()
    .setTargetAspectRatio(screenAspectRatio)
    .setTargetRotation(rotation)

// Use Camera2Interop.Extender to set the video call stream use case.
Camera2Interop.Extender(previewBuilder).setStreamUseCase(videoCallStreamId)

// Bind the Preview UseCase and the corresponding CameraSelector.
val preview = previewBuilder.build()
camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview)

Java

// Set underlying Camera2 stream use case to optimize for video calls.

Long videoCallStreamId =
    CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_CALL.toLong();

// Check available CameraInfos to find the first one that supports
// the video call stream use case.
List<CameraInfo> cameraInfos = cameraProvider.getAvailableCameraInfos();
CameraInfo frontCameraInfo = null;
for (cameraInfo in cameraInfos) {
    Long[] availableStreamUseCases = Camera2CameraInfo.from(cameraInfo)
        .getCameraCharacteristic(
            CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES
        );
    boolean isVideoCallStreamingSupported = Arrays.List(availableStreamUseCases)
                .contains(videoCallStreamId);
    boolean isFrontFacing = (cameraInfo.getLensFacing() ==
                             CameraSelector.LENS_FACING_FRONT);

    if (isVideoCallStreamingSupported && isFrontFacing) {
        frontCameraInfo = cameraInfo;
    }
}

if (frontCameraInfo == null) {
    // Handle case where video call streaming is not supported.
}

CameraSelector cameraSelector = frontCameraInfo.getCameraSelector();

// Start with a Preview Builder.
Preview.Builder previewBuilder = Preview.Builder()
    .setTargetAspectRatio(screenAspectRatio)
    .setTargetRotation(rotation);

// Use Camera2Interop.Extender to set the video call stream use case.
Camera2Interop.Extender(previewBuilder).setStreamUseCase(videoCallStreamId);

// Bind the Preview UseCase and the corresponding CameraSelector.
Preview preview = previewBuilder.build()
Camera camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview)

Ressources supplémentaires

Pour en savoir plus sur CameraX, consultez les ressources supplémentaires suivantes.

Atelier de programmation

  • Premiers pas avec CameraX
  • Exemple de code

  • Applications exemples de CameraX