Premiers pas avec CameraX

1. Avant de commencer

Dans cet atelier de programmation, vous allez apprendre à créer une application d'appareil photo qui utilise CameraX pour afficher un viseur, prendre des photos, enregistrer une vidéo et analyser un flux d'images depuis l'appareil photo.

Pour cela, nous avons introduit dans CameraX le concept de cas d'utilisation, qui vous permettent d'effectuer différentes opérations avec l'appareil photo, comme afficher un viseur ou enregistrer des vidéos.

Conditions préalables

  • Expérience de base du développement Android
  • Connaissance de MediaStore (souhaitable, mais non obligatoire)

Objectifs de l'atelier

  • Apprendre à ajouter les dépendances de CameraX
  • Apprendre à afficher l'aperçu de la caméra dans une activité (cas d'utilisation Preview)
  • Créer une application capable de prendre des photos et de les enregistrer dans l'espace de stockage (cas d'utilisation ImageCapture)
  • Apprendre à analyser les images de l'appareil photo en temps réel (cas d'utilisation de ImageAnalysis)
  • Apprendre à enregistrer une vidéo avec MediaStore (cas d'utilisation VideoCapture)

Ce dont vous avez besoin

  • Un appareil Android ou un émulateur Android Studio :
  • Nous vous recommandons d'utiliser Android 10 ou version ultérieure. Le comportement de MediaStore dépend de la disponibilité d'un espace de stockage cloisonné.
  • Avec Android Emulator**, nous vous recommandons d'utiliser un appareil virtuel Android basé sur Android 11 ou version ultérieure**.
  • Notez que CameraX requiert le niveau d'API minimal 21.
  • Android Studio Arctic Fox 2020.3.1 ou version ultérieure
  • Compréhension de Kotlin et d'Android ViewBinding

2. Créer le projet

  1. Dans Android Studio, créez un projet et sélectionnez Empty Activity (Activité vide) lorsque vous y êtes invité.

ed0f21e863f9e38f.png

  1. Ensuite, nommez l'application "CameraXApp", puis confirmez le nom du package ou remplacez-le par "com.android.example.cameraxapp". Choisissez le langage Kotlin et définissez le niveau d'API minimal sur 21 (niveau minimum requis pour CameraX). Pour les versions plus anciennes d'Android Studio, veillez à inclure la prise en charge des artefacts AndroidX.

10f0a12f6c8b997c.png

Ajouter les dépendances Gradle

  1. Ouvrez le fichier build.gradle du module CameraXApp.app et ajoutez les dépendances de CameraX :
dependencies {
  def camerax_version = "1.1.0-beta01"
  implementation "androidx.camera:camera-core:${camerax_version}"
  implementation "androidx.camera:camera-camera2:${camerax_version}"
  implementation "androidx.camera:camera-lifecycle:${camerax_version}"
  implementation "androidx.camera:camera-video:${camerax_version}"

  implementation "androidx.camera:camera-view:${camerax_version}"
  implementation "androidx.camera:camera-extensions:${camerax_version}"
}
  1. CameraX nécessite certaines méthodes qui font partie de Java 8. Nous devons donc définir nos options de compilation en conséquence. À la fin du bloc android, juste après buildTypes, ajoutez le code suivant :
compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}
  1. Cet atelier de programmation utilisant ViewBinding, activez celui-ci à l'aide du code suivant (à la fin du bloc android{}) :
buildFeatures {
   viewBinding true
}

Lorsque vous y êtes invité, cliquez sur Sync Now (Synchroniser). Vous êtes maintenant prêt à utiliser CameraX dans l'application.

Créer la mise en page de l'atelier de programmation

Dans l'UI de cet atelier de programmation, nous utilisons les éléments suivants :

  • Un PreviewView CameraX (pour prévisualiser l'image/la vidéo de l'appareil photo)
  • Un bouton standard pour contrôler la capture d'image
  • Un bouton standard pour démarrer ou arrêter l'enregistrement vidéo
  • Une indication verticale pour positionner les deux boutons

Remplacez la mise en page par défaut par le code suivant :

  1. Ouvrez le fichier de mise en page activity_main à l'emplacement res/layout/activity_main.xml et remplacez-le par le code suivant :
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <androidx.camera.view.PreviewView
       android:id="@+id/viewFinder"
       android:layout_width="match_parent"
       android:layout_height="match_parent" />

   <Button
       android:id="@+id/image_capture_button"
       android:layout_width="110dp"
       android:layout_height="110dp"
       android:layout_marginBottom="50dp"
       android:layout_marginEnd="50dp"
       android:elevation="2dp"
       android:text="@string/take_photo"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintEnd_toStartOf="@id/vertical_centerline" />

   <Button
       android:id="@+id/video_capture_button"
       android:layout_width="110dp"
       android:layout_height="110dp"
       android:layout_marginBottom="50dp"
       android:layout_marginStart="50dp"
       android:elevation="2dp"
       android:text="@string/start_capture"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintStart_toEndOf="@id/vertical_centerline" />

   <androidx.constraintlayout.widget.Guideline
       android:id="@+id/vertical_centerline"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:orientation="vertical"
       app:layout_constraintGuide_percent=".50" />

</androidx.constraintlayout.widget.ConstraintLayout>
  1. Mettez à jour le fichier res/values/strings.xml avec les éléments suivants :
<resources>
   <string name="app_name">CameraXApp</string>
   <string name="take_photo">Take Photo</string>
   <string name="start_capture">Start Capture</string>
   <string name="stop_capture">Stop Capture</string>
</resources>

Configurer MainActivity.kt

  1. Remplacez le code dans MainActivity.kt par le code ci-dessous, mais ne modifiez pas le nom du package. Ce code inclut les instructions d'importation, les variables à instancier, les fonctions à implémenter et les constantes.

onCreate() a déjà été implémenté pour que nous puissions vérifier les autorisations de l'appareil photo, démarrer l'appareil photo, définir le onClickListener() pour les boutons de capture de photo et d'enregistrement vidéo, et implémenter cameraExecutor. Même si onCreate() est implémenté pour vous, l'appareil photo ne fonctionnera pas tant que nous n'aurons pas implémenté les méthodes du fichier.

package com.android.example.cameraxapp

import android.Manifest
import android.content.ContentValues
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.ImageCapture
import androidx.camera.video.Recorder
import androidx.camera.video.Recording
import androidx.camera.video.VideoCapture
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.android.example.cameraxapp.databinding.ActivityMainBinding
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import android.widget.Toast
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.core.Preview
import androidx.camera.core.CameraSelector
import android.util.Log
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.ImageProxy
import androidx.camera.video.FallbackStrategy
import androidx.camera.video.MediaStoreOutputOptions
import androidx.camera.video.Quality
import androidx.camera.video.QualitySelector
import androidx.camera.video.VideoRecordEvent
import androidx.core.content.PermissionChecker
import java.nio.ByteBuffer
import java.text.SimpleDateFormat
import java.util.Locale

typealias LumaListener = (luma: Double) -> Unit

class MainActivity : AppCompatActivity() {
   private lateinit var viewBinding: ActivityMainBinding

   private var imageCapture: ImageCapture? = null

   private var videoCapture: VideoCapture<Recorder>? = null
   private var recording: Recording? = null

   private lateinit var cameraExecutor: ExecutorService

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       viewBinding = ActivityMainBinding.inflate(layoutInflater)
       setContentView(viewBinding.root)

       // Request camera permissions
       if (allPermissionsGranted()) {
           startCamera()
       } else {
           ActivityCompat.requestPermissions(
               this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
       }

       // Set up the listeners for take photo and video capture buttons
       viewBinding.imageCaptureButton.setOnClickListener { takePhoto() }
       viewBinding.videoCaptureButton.setOnClickListener { captureVideo() }

       cameraExecutor = Executors.newSingleThreadExecutor()
   }

   private fun takePhoto() {}

   private fun captureVideo() {}

   private fun startCamera() {}

   private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
       ContextCompat.checkSelfPermission(
           baseContext, it) == PackageManager.PERMISSION_GRANTED
   }

   override fun onDestroy() {
       super.onDestroy()
       cameraExecutor.shutdown()
   }

   companion object {
       private const val TAG = "CameraXApp"
       private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
       private const val REQUEST_CODE_PERMISSIONS = 10
       private val REQUIRED_PERMISSIONS =
           mutableListOf (
               Manifest.permission.CAMERA,
               Manifest.permission.RECORD_AUDIO
           ).apply {
               if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
                   add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
               }
           }.toTypedArray()
   }
}

3. Demander les autorisations nécessaires

Pour que l'appareil photo s'ouvre, l'utilisateur doit autoriser l'application à s'ouvrir. L'autorisation d'accès au micro est également requise pour enregistrer du contenu audio. Sur Android 9 (P) et versions antérieures, MediaStore a besoin de l'autorisation d'écrire sur un espace de stockage externe. Dans cette étape, nous allons implémenter ces autorisations.

  1. Ouvrez AndroidManifest.xml et ajoutez ces lignes avant la balise application.
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
   android:maxSdkVersion="28" />

Ajouter android.hardware.camera.any permet de s'assurer que l'appareil est équipé d'une caméra. Spécifier .any permet d'indiquer qu'il peut s'agir d'une caméra avant ou arrière.

  1. Copiez ce code dans MainActivity.kt.. La liste ci-dessous explique le code que nous venons de copier.
override fun onRequestPermissionsResult(
   requestCode: Int, permissions: Array<String>, grantResults:
   IntArray) {
   if (requestCode == REQUEST_CODE_PERMISSIONS) {
       if (allPermissionsGranted()) {
           startCamera()
       } else {
           Toast.makeText(this,
               "Permissions not granted by the user.",
               Toast.LENGTH_SHORT).show()
           finish()
       }
   }
}
  • Vérifiez que le code de requête est correct. Si ce n'est pas le cas, vous pouvez l'ignorer.
if (requestCode == REQUEST_CODE_PERMISSIONS) {

}
  • Si les autorisations sont accordées, appelez startCamera().
if (allPermissionsGranted()) {
   startCamera()
}
  • Si les autorisations ne sont pas accordées, présentez un toast pour avertir l'utilisateur que les autorisations n'ont pas été accordées.
else {
   Toast.makeText(this,
       "Permissions not granted by the user.",
       Toast.LENGTH_SHORT).show()
   finish()
}
  1. Exécutez l'application.

Elle doit maintenant demander l'autorisation d'utiliser la caméra et le micro :

dcdf8aa3d87e74be.png

4. Implémenter le cas d'utilisation Preview

Dans une application d'appareil photo, le viseur est utilisé pour permettre à l'utilisateur d'afficher un aperçu de la photo qu'il va prendre. Nous allons implémenter un viseur à l'aide de la classe Preview de CameraX.

Pour utiliser Preview, nous devons d'abord définir une configuration, qui sert ensuite à créer une instance du cas d'utilisation. L'instance résultante sera alors associée au cycle de vie de CameraX.

  1. Copiez ce code dans la fonction startCamera().

La liste ci-dessous décrit le code que nous venons de copier.

private fun startCamera() {
   val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

   cameraProviderFuture.addListener({
       // Used to bind the lifecycle of cameras to the lifecycle owner
       val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

       // Preview
       val preview = Preview.Builder()
          .build()
          .also {
              it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
          }

       // Select back camera as a default
       val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

       try {
           // Unbind use cases before rebinding
           cameraProvider.unbindAll()

           // Bind use cases to camera
           cameraProvider.bindToLifecycle(
               this, cameraSelector, preview)

       } catch(exc: Exception) {
           Log.e(TAG, "Use case binding failed", exc)
       }

   }, ContextCompat.getMainExecutor(this))
}
  • Créez une instance de ProcessCameraProvider. Elle servira à associer le cycle de vie de l'appareil photo au propriétaire du cycle de vie. Comme CameraX tient compte du cycle de vie, cela vous évite d'avoir à ouvrir et fermer l'appareil photo.
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
  • Ajoutez un écouteur à cameraProviderFuture. Ajoutez Runnable comme premier argument. Nous le remplirons plus tard. Ajoutez ContextCompat.getMainExecutor() comme deuxième argument. Cela renvoie un Executor qui s'exécute sur le thread principal.
cameraProviderFuture.addListener(Runnable {}, ContextCompat.getMainExecutor(this))
  • Dans Runnable, ajoutez un ProcessCameraProvider. Il sert à associer le cycle de vie de notre appareil photo à LifecycleOwner dans le processus de l'application.
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
  • Initialisez l'objet Preview, appelez-le sur la compilation, récupérez un fournisseur de surface à partir du viseur, puis définissez-le sur l'aperçu.
val preview = Preview.Builder()
   .build()
   .also {
       it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
   }
  • Créez un objet CameraSelector, puis sélectionnez DEFAULT_BACK_CAMERA.
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
  • Créez un bloc try. Dans ce bloc, assurez-vous que rien n'est lié à cameraProvider, puis associez cameraSelector et l'objet d'aperçu à cameraProvider.
try {
   cameraProvider.unbindAll()
   cameraProvider.bindToLifecycle(
       this, cameraSelector, preview)
}
  • Ce code peut échouer dans certains cas, par exemple si l'application ne se trouve plus au premier plan. En cas de défaillance, encapsulez ce code dans un bloc catch pour consigner cette erreur.
catch(exc: Exception) {
      Log.e(TAG, "Use case binding failed", exc)
}
  1. Exécutez l'application. Vous pouvez à présent voir un aperçu de l'appareil photo !

d61a4250f6a3ed35.png

5. Implémenter le cas d'utilisation ImageCapture

Les autres cas d'utilisation fonctionnent de manière très semblable à Preview. Tout d'abord, nous définissons un objet de configuration qui permet d'instancier l'objet de cas d'utilisation réel. Pour prendre des photos, vous allez implémenter la méthode takePhoto(), qui est appelée lorsque vous appuyez sur le bouton Take photo (Prendre une photo).

  1. Copiez ce code dans la méthode takePhoto().

La liste ci-dessous décrit le code que nous venons de copier.

private fun takePhoto() {
   // Get a stable reference of the modifiable image capture use case
   val imageCapture = imageCapture ?: return

   // Create time stamped name and MediaStore entry.
   val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
              .format(System.currentTimeMillis())
   val contentValues = ContentValues().apply {
       put(MediaStore.MediaColumns.DISPLAY_NAME, name)
       put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
       if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
           put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
       }
   }

   // Create output options object which contains file + metadata
   val outputOptions = ImageCapture.OutputFileOptions
           .Builder(contentResolver,
                    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                    contentValues)
           .build()

   // Set up image capture listener, which is triggered after photo has
   // been taken
   imageCapture.takePicture(
       outputOptions,
       ContextCompat.getMainExecutor(this),
       object : ImageCapture.OnImageSavedCallback {
           override fun onError(exc: ImageCaptureException) {
               Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
           }

           override fun
               onImageSaved(output: ImageCapture.OutputFileResults){
               val msg = "Photo capture succeeded: ${output.savedUri}"
               Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
               Log.d(TAG, msg)
           }
       }
   )
}
  • Tout d'abord, obtenez une référence au cas d'utilisation ImageCapture. Si le cas d'utilisation est nul, quittez la fonction. C'est le cas si vous appuyez sur le bouton pour prendre une photo avant de configurer la capture d'image. Sans l'instruction return, l'application planterait si elle avait la valeur null.
val imageCapture = imageCapture ?: return
  • Ensuite, créez une valeur de contenu MediaStore pour stocker l'image. Utilisez un code temporel afin que le nom à afficher dans MediaStore soit unique.
   val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
              .format(System.currentTimeMillis())
   val contentValues = ContentValues().apply {
       put(MediaStore.MediaColumns.DISPLAY_NAME, name)
       put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
       if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
           put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
       }
   }
  • Créez un objet OutputFileOptions. C'est dans cet objet que nous pouvons spécifier les paramètres du résultat. Nous voulons que le résultat soit enregistré dans le MediaStore pour que d'autres applications puissent l'afficher. Ajoutons donc l'entrée MediaStore.
val outputOptions = ImageCapture.OutputFileOptions
       .Builder(contentResolver,
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                contentValues)
       .build()
  • Appelez takePicture() sur l'objet imageCapture. Transmettez outputOptions, l'exécuteur et un rappel pour le moment où l'image est enregistrée. Vous paramétrerez le rappel plus tard.
imageCapture.takePicture(
   outputOptions, ContextCompat.getMainExecutor(this),
   object : ImageCapture.OnImageSavedCallback {}
)
  • Si la capture d'image ou son enregistrement échouent, ajoutez une erreur pour consigner cet échec.
override fun onError(exc: ImageCaptureException) {
   Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
}
  • Si la capture réussie, cela signifie que la photo a bien été prise. Enregistrez la photo dans le fichier que nous avons créé précédemment, présentez un toast pour informer l'utilisateur que la photo a bien été prise et imprimez une instruction de journalisation.
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
   val savedUri = Uri.fromFile(photoFile)
   val msg = "Photo capture succeeded: $savedUri"
   Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
   Log.d(TAG, msg)
}
  1. Accédez à la méthode startCamera() et copiez ce code sous le code pour obtenir un aperçu.
imageCapture = ImageCapture.Builder().build()
  1. Enfin, mettez à jour l'appel à bindToLifecycle() dans le bloc try pour inclure le nouveau cas d'utilisation :
cameraProvider.bindToLifecycle(
   this, cameraSelector, preview, imageCapture)

La méthode ressemble à présent à ceci :

private fun startCamera() {
   val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

   cameraProviderFuture.addListener({
       // Used to bind the lifecycle of cameras to the lifecycle owner
       val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

       // Preview
       val preview = Preview.Builder()
           .build()
           .also {
                 it.setSurfaceProvider(viewFinder.surfaceProvider)
           }

       imageCapture = ImageCapture.Builder()
           .build()

       // Select back camera as a default
       val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

       try {
           // Unbind use cases before rebinding
           cameraProvider.unbindAll()

           // Bind use cases to camera
           cameraProvider.bindToLifecycle(
               this, cameraSelector, preview, imageCapture)

       } catch(exc: Exception) {
           Log.e(TAG, "Use case binding failed", exc)
       }

   }, ContextCompat.getMainExecutor(this))
}
  1. Exécutez de nouveau l'application et appuyez sur Take Photo (Prendre une photo). Un toast devrait s'afficher à l'écran et un message devrait être consigné dans les journaux.

54292eaa4ce3be0a.png

Afficher la photo

Maintenant que les photos que nous venons de prendre sont enregistrées dans MediaStore, nous pouvons utiliser n'importe quelle application MediaStore pour les afficher. Par exemple, avec l'application Google Photos, procédez ainsi :

  1. Lancez Google Photos Photos.
  2. Si vous êtes connecté à l'application Photos avec votre compte, appuyez sur "Library" (Bibliothèque) pour voir les fichiers multimédias triés. Notre dossier s'intitule "CameraX-Image".

8e884489ca2599e9.png 9ca38ee62f08ef6f.png

  1. Appuyez sur l'icône d'image pour afficher la photo complète. Appuyez sur le bouton Plus Plus en haut à droite pour afficher les détails de la photo prise.

55e1a442ab5f25e7.png 70a8b27a76523f56.png

Si votre but est de créer une application d'appareil photo simple pour prendre des photos, vous avez terminé. Vous n'avez rien d'autre à faire. Si vous souhaitez implémenter un analyseur d'images, passez à la suite.

6. Implémenter le cas d'utilisation ImageAnalysis

La fonctionnalité ImageAnalysis est un excellent moyen de rendre notre application d'appareil photo plus intéressante. Elle nous permet de définir une classe personnalisée qui implémente l'interface ImageAnalysis.Analyzer et qui sera appelée avec les images prises par l'appareil photo. Nous n'aurons pas à gérer l'état de la session de l'appareil photo ni même à supprimer les images. L'association au cycle de vie souhaité pour notre application est suffisante, comme avec d'autres composants tenant compte du cycle de vie.

  1. Ajoutez cet analyseur en tant que classe interne dans MainActivity.kt. L'analyseur consigne la luminosité moyenne de l'image. Pour créer un analyseur, nous devons remplacer la fonction analyze dans une classe qui implémente l'interface ImageAnalysis.Analyzer.
private class LuminosityAnalyzer(private val listener: LumaListener) : ImageAnalysis.Analyzer {

   private fun ByteBuffer.toByteArray(): ByteArray {
       rewind()    // Rewind the buffer to zero
       val data = ByteArray(remaining())
       get(data)   // Copy the buffer into a byte array
       return data // Return the byte array
   }

   override fun analyze(image: ImageProxy) {

       val buffer = image.planes[0].buffer
       val data = buffer.toByteArray()
       val pixels = data.map { it.toInt() and 0xFF }
       val luma = pixels.average()

       listener(luma)

       image.close()
   }
}

Avec notre classe implémentant l'interface ImageAnalysis.Analyzer, il nous suffit d'instancier une instance de LuminosityAnalyzer dans ImageAnalysis,, comme dans les autres cas d'utilisation, puis de mettre à jour de nouveau la fonction startCamera() avant d'appeler CameraX.bindToLifecycle() :

  1. Dans la méthode startCamera(), ajoutez ce code sous le code imageCapture.
val imageAnalyzer = ImageAnalysis.Builder()
   .build()
   .also {
       it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
           Log.d(TAG, "Average luminosity: $luma")
       })
   }
  1. Mettez à jour l'appel bindToLifecycle() sur cameraProvider pour inclure imageAnalyzer.
cameraProvider.bindToLifecycle(
   this, cameraSelector, preview, imageCapture, imageAnalyzer)

La méthode complète se présente comme suit :

private fun startCamera() {
   val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

   cameraProviderFuture.addListener({
       // Used to bind the lifecycle of cameras to the lifecycle owner
       val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

       // Preview
       val preview = Preview.Builder()
           .build()
           .also {
               it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
           }

       imageCapture = ImageCapture.Builder()
           .build()

       val imageAnalyzer = ImageAnalysis.Builder()
           .build()
           .also {
               it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
                   Log.d(TAG, "Average luminosity: $luma")
               })
           }

       // Select back camera as a default
       val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

       try {
           // Unbind use cases before rebinding
           cameraProvider.unbindAll()

           // Bind use cases to camera
           cameraProvider.bindToLifecycle(
               this, cameraSelector, preview, imageCapture, imageAnalyzer)

       } catch(exc: Exception) {
           Log.e(TAG, "Use case binding failed", exc)
       }

   }, ContextCompat.getMainExecutor(this))
}
  1. Exécutez l'application. Un message semblable à celui-ci sera généré dans logcat chaque seconde environ.
D/CameraXApp: Average luminosity: ...

7. Implémenter le cas d'utilisation VideoCapture

Le cas d'utilisation VideoCapture a été ajouté dans la version 1.1.0-alpha10 de CameraX, et d'autres améliorations ont été apportées depuis. Notez que l'API VideoCapture prend en charge de nombreuses fonctionnalités de capture vidéo. Par souci de simplicité, seule la capture vidéo et audio sur MediaStore est présentée dans cet atelier de programmation.

  1. Copiez ce code dans la méthode captureVideo(). Il permet de contrôler le démarrage et l'arrêt de notre cas d'utilisation VideoCapture. La liste ci-dessous décrit le code que nous venons de copier.
// Implements VideoCapture use case, including start and stop capturing.
private fun captureVideo() {
   val videoCapture = this.videoCapture ?: return

   viewBinding.videoCaptureButton.isEnabled = false

   val curRecording = recording
   if (curRecording != null) {
       // Stop the current recording session.
       curRecording.stop()
       recording = null
       return
   }

   // create and start a new recording session
   val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
              .format(System.currentTimeMillis())
   val contentValues = ContentValues().apply {
       put(MediaStore.MediaColumns.DISPLAY_NAME, name)
       put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
       if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
           put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video")
       }
   }

   val mediaStoreOutputOptions = MediaStoreOutputOptions
       .Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
       .setContentValues(contentValues)
       .build()
   recording = videoCapture.output
       .prepareRecording(this, mediaStoreOutputOptions)
       .apply {
           if (PermissionChecker.checkSelfPermission(this@MainActivity,
                   Manifest.permission.RECORD_AUDIO) ==
               PermissionChecker.PERMISSION_GRANTED)
           {
               withAudioEnabled()
           }
       }
       .start(ContextCompat.getMainExecutor(this)) { recordEvent ->
           when(recordEvent) {
               is VideoRecordEvent.Start -> {
                   viewBinding.videoCaptureButton.apply {
                       text = getString(R.string.stop_capture)
                       isEnabled = true
                   }
               }
               is VideoRecordEvent.Finalize -> {
                   if (!recordEvent.hasError()) {
                       val msg = "Video capture succeeded: " +
                           "${recordEvent.outputResults.outputUri}"
                       Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT)
                            .show()
                       Log.d(TAG, msg)
                   } else {
                       recording?.close()
                       recording = null
                       Log.e(TAG, "Video capture ends with error: " +
                           "${recordEvent.error}")
                   }
                   viewBinding.videoCaptureButton.apply {
                       text = getString(R.string.start_capture)
                       isEnabled = true
                   }
               }
           }
       }
}
  • Vérifiez si le cas d'utilisation VideoCapture a été créé. Si ce n'est pas le cas, ne faites rien.
val videoCapture = videoCapture ?: return
  • Désactivez l'interface utilisateur jusqu'à ce que l'action de la requête soit effectuée par CameraX. Elle sera réactivée à une étape ultérieure dans le VideoRecordListener enregistré.
viewBinding.videoCaptureButton.isEnabled = false
  • Si un enregistrement est en cours, arrêtez-le et libérez le recording actuel. Nous serons avertis lorsque le fichier vidéo capturé sera prêt à être utilisé par notre application.
val curRecording = recording
if (curRecording != null) {
    curRecording.stop()
    recording = null
    return
}
  • Pour commencer l'enregistrement, nous créons une session d'enregistrement. Nous commençons par créer l'objet de contenu vidéo MediaStore prévu, avec un code temporel système comme nom à afficher (afin de pouvoir capturer plusieurs vidéos).
val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
           .format(System.currentTimeMillis())
val contentValues = ContentValues().apply {
       put(MediaStore.MediaColumns.DISPLAY_NAME, name)
       put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
       if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
           put(MediaStore.Video.Media.RELATIVE_PATH,
               "Movies/CameraX-Video")
       }
}
val mediaStoreOutputOptions = MediaStoreOutputOptions
      .Builder(contentResolver,
               MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
  • Définissez la vidéo contentValues créée sur MediaStoreOutputOptions.Builder, puis créez l'instance MediaStoreOutputOptions.
    .setContentValues(contentValues)
    .build()
    videoCapture
    .output
    .prepareRecording(this, mediaStoreOutputOptions)
.withAudioEnabled()
  • Activez Audio dans cet enregistrement.
.apply {
   if (PermissionChecker.checkSelfPermission(this@MainActivity,
           Manifest.permission.RECORD_AUDIO) ==
       PermissionChecker.PERMISSION_GRANTED)
   {
       withAudioEnabled()
   }
}
.start(ContextCompat.getMainExecutor(this)) { recordEvent ->
   //lambda event listener
}
  • Lorsque la demande d'enregistrement est lancée par l'appareil photo, remplacez le texte du bouton "Start Capture" (Démarrer la capture) par "Stop capture" (Arrêter la capture).
is VideoRecordEvent.Start -> {
    viewBinding.videoCaptureButton.apply {
        text = getString(R.string.stop_capture)
        isEnabled = true
    }
}
  • Une fois l'enregistrement actif terminé, envoyez un toast à l'utilisateur, rebasculez le texte du bouton sur "Start Capture" (Démarrer la capture), puis réactivez-le :
is VideoRecordEvent.Finalize -> {
   if (!recordEvent.hasError()) {
       val msg = "Video capture succeeded: " +
                 "${recordEvent.outputResults.outputUri}"
       Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT)
            .show()
       Log.d(TAG, msg)
   } else {
       recording?.close()
       recording = null
       Log.e(TAG, "Video capture succeeded: " +
                  "${recordEvent.outputResults.outputUri}")
   }
   viewBinding.videoCaptureButton.apply {
       text = getString(R.string.start_capture)
       isEnabled = true
   }
}
  1. Dans startCamera(), insérez le code suivant après la ligne de création preview. Le cas d'utilisation VideoCapture est alors créé.
val recorder = Recorder.Builder()
   .setQualitySelector(QualitySelector.from(Quality.HIGHEST))
   .build()
videoCapture = VideoCapture.withOutput(recorder)
  1. (Facultatif) Dans startCamera() également, désactivez les cas d'utilisation imageCapture et imageAnalyzer en supprimant ou en ajoutant un commentaire au code suivant :
/* comment out ImageCapture and ImageAnalyzer use cases
imageCapture = ImageCapture.Builder().build()

val imageAnalyzer = ImageAnalysis.Builder()
   .build()
   .also {
       it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
           Log.d(TAG, "Average luminosity: $luma")
       })
   }
*/
  1. Associez les cas d'utilisation Preview + VideoCapture au cycle de vie de l'appareil photo. Toujours dans startCamera(), remplacez l'appel cameraProvider.bindToLifecycle() par le code suivant :
   // Bind use cases to camera
   cameraProvider.bindToLifecycle(this, cameraSelector, preview, videoCapture)

À ce stade, startCamera() devrait se présenter comme suit :

   val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

   cameraProviderFuture.addListener({
       // Used to bind the lifecycle of cameras to the lifecycle owner
       val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

       // Preview
       val preview = Preview.Builder()
           .build()
           .also {
               it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
           }

       val recorder = Recorder.Builder()
           .setQualitySelector(QualitySelector.from(Quality.HIGHEST))
           .build()
       videoCapture = VideoCapture.withOutput(recorder)

       /*
       imageCapture = ImageCapture.Builder().build()

       val imageAnalyzer = ImageAnalysis.Builder()
           .build()
           .also {
               it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
                   Log.d(TAG, "Average luminosity: $luma")
               })
           }
       */

       // Select back camera as a default
       val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

       try {
           // Unbind use cases before rebinding
           cameraProvider.unbindAll()

           // Bind use cases to camera
           cameraProvider
               .bindToLifecycle(this, cameraSelector, preview, videoCapture)
       } catch(exc: Exception) {
           Log.e(TAG, "Use case binding failed", exc)
       }

   }, ContextCompat.getMainExecutor(this))
}
  1. Compilez et exécutez. L'interface utilisateur des étapes précédentes doit s'afficher.
  2. Enregistrez quelques extraits vidéo :
  • Appuyez sur le bouton "START CAPTURE" (DÉMARRER LA CAPTURE). Notez que le texte du bouton change et est remplacé par "STOP CAPTURE" (ARRÊTER LA CAPTURE).
  • Enregistrez une vidéo de quelques secondes/minutes.
  • Appuyez sur le bouton "STOP CAPTURE" (ARRÊTER LA CAPTURE). Il s'agit du même bouton que pour lancer la capture.

ef2a6005defc4977.png 8acee41fd0f4af0f.png

Regarder la vidéo (comme pour afficher le fichier image de capture)

Nous allons utiliser l'application Google Photos pour visionner la vidéo enregistrée :

  1. Lancez Google Photos Photos.
  2. Appuyez sur "Library" (Bibliothèque) pour afficher les fichiers multimédias triés. Appuyez sur l'icône de dossier "CameraX-Video" pour afficher la liste des extraits vidéo disponibles.

71f07e32d5f4f268.png 596819ad391fac37.png

  1. Appuyez sur l'icône pour regarder l'extrait vidéo que vous venez d'enregistrer. Une fois la lecture terminée, appuyez sur le bouton Plus Plus en haut à droite pour consulter les détails de l'extrait.

7c7125726af9e429.png 44da18b15ad2f607.png

C'est tout ce dont nous avons besoin pour enregistrer une vidéo ! Mais le cas d'utilisation VideoCapture de CameraX possède de nombreuses autres fonctionnalités, par exemple :

  • la pause/reprise de l'enregistrement,
  • la capture sur File ou FileDescriptor,
  • et bien d'autres.

Pour apprendre à utiliser ces fonctionnalités, veuillez consulter la documentation officielle.

8. (Facultatif) Combiner VideoCapture et d'autres cas d'utilisation

L'étape précédente sur VideoCapture montrait comment combiner les cas d'utilisation Preview et VideoCapture, tous deux pris en charge par tous les appareils, comme indiqué dans le tableau des fonctionnalités des appareils. Dans cette étape, nous allons ajouter le cas d'utilisation ImageCapture à la combinaison de cas VideoCapture + Preview existante pour illustrer la combinaison Preview + ImageCapture + VideoCapture.

  1. Dans le code de l'étape précédente, annulez la mise en commentaire et activez la création de imageCapture dans startCamera() :
imageCapture = ImageCapture.Builder().build()
  1. Ajoutez une FallbackStrategy à la création QualitySelector existante. Cela permet à CameraX de détecter une résolution compatible si le niveau Quality.HIGHEST requis n'est pas compatible avec le cas d'utilisation imageCapture.
.setQualitySelector(QualitySelector.from(Quality.HIGHEST,
    FallbackStrategy.higherQualityOrLowerThan(Quality.SD)))
  1. Toujours dans startCamera(), associez le cas d'utilisation imageCapture aux cas d'utilisation Preview et VideoCapture existants (remarque : n'associez pas imageAnalyzer, car la combinaison preview + imageCapture + videoCapture + imageAnalysis n'est pas prise en charge) :
cameraProvider.bindToLifecycle(
   this, cameraSelector, preview, imageCapture, videoCapture)

La fonction startCamera() finale ressemblera à ceci :

private fun startCamera() {
       val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

       cameraProviderFuture.addListener({
           // Used to bind the lifecycle of cameras to the lifecycle owner
           val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

           // Preview
           val preview = Preview.Builder()
               .build()
               .also {
                   it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
               }
           val recorder = Recorder.Builder()
               .setQualitySelector(QualitySelector.from(Quality.HIGHEST,
                    FallbackStrategy.higherQualityOrLowerThan(Quality.SD)))
               .build()
           videoCapture = VideoCapture.withOutput(recorder)

           imageCapture = ImageCapture.Builder().build()

           /*
           val imageAnalyzer = ImageAnalysis.Builder().build()
               .also {
                   setAnalyzer(
                       cameraExecutor,
                       LuminosityAnalyzer { luma ->
                           Log.d(TAG, "Average luminosity: $luma")
                       }
                   )
               }
           */

           // Select back camera as a default
           val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

           try {
               // Unbind use cases before rebinding
               cameraProvider.unbindAll()

               // Bind use cases to camera
               cameraProvider.bindToLifecycle(
                   this, cameraSelector, preview, imageCapture, videoCapture)

           } catch(exc: Exception) {
               Log.e(TAG, "Use case binding failed", exc)
           }

       }, ContextCompat.getMainExecutor(this))
   }
  1. Compilez et exécutez. L'interface utilisateur des étapes précédentes devrait s'afficher, mais cette fois, les boutons Take Photo (Prendre une photo) et Start Capture (Démarrer la capture) fonctionnent.
  2. Enregistrez une vidéo et prenez une photo :
  • Appuyez sur le bouton START CAPTURE (DÉMARRER LA CAPTURE) pour commencer à enregistrer une vidéo.
  • Appuyez sur TAKE PHOTO (PRENDRE UNE PHOTO) pour prendre une photo.
  • Attendez la fin de la capture d'image (un toast devrait apparaître, comme précédemment).
  • Appuyez sur le bouton STOP CAPTURE (ARRÊTER LA CAPTURE) pour arrêter l'enregistrement.

Vous venez de prendre une photo alors que l'aperçu et l'enregistrement vidéo sont actifs !

ef2a6005defc4977.png 16bc70ec3346fa66.png

  1. Utilisez l'application Google Photos pour afficher les fichiers image et vidéo capturés, comme nous l'avons fait aux étapes précédentes. Cette fois, vous devriez voir deux photos et deux extraits vidéo.

3f3feb19c8c73532.png

  1. (Facultatif) Remplacez imageCapture par le cas d'utilisation ImageAnalyzer dans les étapes 1 à 4 ci-dessus : nous utiliserons la combinaison Preview + ImageAnalysis + VideoCapture. À nouveau, notez que la combinaison Preview + Analysis + ImageCapture + VideoCapture peut ne pas être prise en charge, même avec les appareils photo LEVEL_3.

9. Félicitations !

Vous avez réalisé les opérations suivantes dans une nouvelle application Android :

  • Inclusion des dépendances CameraX dans un nouveau projet
  • Affichage d'un viseur d'appareil photo à l'aide du cas d'utilisation Preview
  • Implémentation de la capture de photos et de l'enregistrement d'images dans un espace de stockage à l'aide du cas d'utilisation ImageCapture
  • Implémentation de l'analyse des images de l'appareil photo en temps réel à l'aide du cas d'utilisation ImageAnalysis
  • Implémentation de la capture vidéo à l'aide du cas d'utilisation VideoCapture

Pour en savoir plus sur CameraX et ces fonctionnalités, consultez la documentation ou clonez l'exemple officiel.