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
- Dans Android Studio, créez un projet et sélectionnez Empty Activity (Activité vide) lorsque vous y êtes invité.
- 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.
Ajouter les dépendances Gradle
- Ouvrez le fichier
build.gradle
du moduleCameraXApp.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}"
}
- 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èsbuildTypes
, ajoutez le code suivant :
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
- 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 :
- Ouvrez le fichier de mise en page
activity_main
à l'emplacementres/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>
- 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
- 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.
- Ouvrez
AndroidManifest.xml
et ajoutez ces lignes avant la baliseapplication
.
<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.
- 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()
}
- Exécutez l'application.
Elle doit maintenant demander l'autorisation d'utiliser la caméra et le micro :
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.
- 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
. AjoutezRunnable
comme premier argument. Nous le remplirons plus tard. AjoutezContextCompat
.getMainExecutor()
comme deuxième argument. Cela renvoie unExecutor
qui s'exécute sur le thread principal.
cameraProviderFuture.addListener(Runnable {}, ContextCompat.getMainExecutor(this))
- Dans
Runnable
, ajoutez unProcessCameraProvider
. 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électionnezDEFAULT_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 associezcameraSelector
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)
}
- Exécutez l'application. Vous pouvez à présent voir un aperçu de l'appareil photo !
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).
- 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'instructionreturn
, l'application planterait si elle avait la valeurnull
.
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'objetimageCapture
. TransmettezoutputOptions
, 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)
}
- Accédez à la méthode
startCamera()
et copiez ce code sous le code pour obtenir un aperçu.
imageCapture = ImageCapture.Builder().build()
- Enfin, mettez à jour l'appel à
bindToLifecycle()
dans le bloctry
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))
}
- 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.
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 :
- Lancez Google Photos .
- 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"
.
- Appuyez sur l'icône d'image pour afficher la photo complète. Appuyez sur le bouton Plus en haut à droite pour afficher les détails de la photo prise.
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.
- 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 fonctionanalyze
dans une classe qui implémente l'interfaceImageAnalysis.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()
:
- Dans la méthode
startCamera()
, ajoutez ce code sous le codeimageCapture
.
val imageAnalyzer = ImageAnalysis.Builder()
.build()
.also {
it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
Log.d(TAG, "Average luminosity: $luma")
})
}
- Mettez à jour l'appel
bindToLifecycle()
surcameraProvider
pour inclureimageAnalyzer
.
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))
}
- 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.
- Copiez ce code dans la méthode
captureVideo()
. Il permet de contrôler le démarrage et l'arrêt de notre cas d'utilisationVideoCapture
. 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")
}
}
- Créez un
MediaStoreOutputOptions.Builder
avec l'option de contenu externe.
val mediaStoreOutputOptions = MediaStoreOutputOptions
.Builder(contentResolver,
MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
- Définissez la vidéo
contentValues
créée surMediaStoreOutputOptions.Builder
, puis créez l'instanceMediaStoreOutputOptions
.
.setContentValues(contentValues)
.build()
- Configurez l'option de sortie sur le
Recorder
deVideoCapture<Recorder>
et activez l'enregistrement audio :
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()
}
}
- Démarrez ce nouvel enregistrement et enregistrez un écouteur lambda
VideoRecordEvent
.
.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
}
}
- Dans
startCamera()
, insérez le code suivant après la ligne de créationpreview
. Le cas d'utilisationVideoCapture
est alors créé.
val recorder = Recorder.Builder()
.setQualitySelector(QualitySelector.from(Quality.HIGHEST))
.build()
videoCapture = VideoCapture.withOutput(recorder)
- (Facultatif) Dans
startCamera()
également, désactivez les cas d'utilisationimageCapture
etimageAnalyzer
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")
})
}
*/
- Associez les cas d'utilisation
Preview
+VideoCapture
au cycle de vie de l'appareil photo. Toujours dansstartCamera()
, remplacez l'appelcameraProvider.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))
}
- Compilez et exécutez. L'interface utilisateur des étapes précédentes doit s'afficher.
- 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.
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 :
- Lancez Google Photos .
- 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.
- 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 en haut à droite pour consulter les détails de l'extrait.
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
ouFileDescriptor
, - 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
.
- Dans le code de l'étape précédente, annulez la mise en commentaire et activez la création de
imageCapture
dansstartCamera()
:
imageCapture = ImageCapture.Builder().build()
- Ajoutez une
FallbackStrategy
à la créationQualitySelector
existante. Cela permet à CameraX de détecter une résolution compatible si le niveauQuality.HIGHEST
requis n'est pas compatible avec le cas d'utilisationimageCapture
.
.setQualitySelector(QualitySelector.from(Quality.HIGHEST,
FallbackStrategy.higherQualityOrLowerThan(Quality.SD)))
- Toujours dans
startCamera()
, associez le cas d'utilisationimageCapture
aux cas d'utilisation Preview et VideoCapture existants (remarque : n'associez pasimageAnalyzer
, car la combinaisonpreview + 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))
}
- 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.
- 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 !
- 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.
- (Facultatif) Remplacez
imageCapture
par le cas d'utilisationImageAnalyzer
dans les étapes 1 à 4 ci-dessus : nous utiliserons la combinaisonPreview
+ImageAnalysis
+VideoCapture
. À nouveau, notez que la combinaisonPreview
+Analysis
+ImageCapture
+VideoCapture
peut ne pas être prise en charge, même avec les appareils photoLEVEL_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.