Réussir l'extraction d'images à partir d'une vidéo sur Android
Nicolas Fez
2 octobre 2025
Introduction
Dans le cadre d’un projet client, j’ai eu besoin de développer une solution capable de lire différents types de sources vidéo et d’en extraire des images pour analyse. Ces sources pouvaient aussi bien provenir d’un fichier local que d’un flux réseau en temps réel, tel qu’un flux RTSP ou RTP transmis via UDP.
Comme tout développeur, la première étape consiste à évaluer la plateforme sur laquelle la solution doit être déployée, puis à se poser une question essentielle :
Quelqu’un a-t-il déjà rencontré ce problème et trouvé une solution ?
Heureusement, dans ce cas précis, la réponse est oui ! 🙂
Objectif de cet article
Cet article propose une approche claire pour comprendre et mettre en œuvre l’extraction d’images à partir de flux vidéo sur Android.
Il débute par un rappel des principes fondamentaux du fonctionnement d’une vidéo, avant de présenter deux solutions concrètes :
- JavaCV, une bibliothèque tierce basée sur FFmpeg ;
- Media3 ExoPlayer, la solution standard du framework Android.
Cette double approche permettra de comparer leurs avantages respectifs et de comprendre comment les intégrer efficacement dans un projet Android.
Comprendre le fonctionnement d’une vidéo
Une vidéo numérique est avant tout une combinaison d’images animées et d’un son synchronisé. Pour qu’elle puisse être lue correctement sur un appareil, ces composants doivent d’abord être séparés, puis décodés avant d’être affichés.
Par exemple, un fichier MP4 contient à la fois des pistes vidéo et audio. Un démultiplexeur (ou demuxer) se charge de séparer ces flux. Une fois séparés, ils restent encodés et nécessitent un décodeur adapté. Parmi les codecs audio les plus répandus, on retrouve AAC, MP3 et Opus, tandis que les codecs vidéo les plus courants sont H.264, HEVC, VP9 et AV1.

Lorsqu’on effectue l’opération inverse — c’est-à-dire la création d’un fichier vidéo —, le processus consiste à encoder les données brutes, puis à muxer (ou multiplexer) les pistes audio et vidéo dans un seul conteneur. Le résultat est un fichier ou un flux prêt à être stocké, partagé ou diffusé.
Outils existants
Grâce à des décennies de travail et de contributions de la communauté multimédia, plusieurs outils puissants ont vu le jour pour traiter ce type d’opérations. Les plus connus sont FFmpeg et GStreamer.
Puisque cet article s’appuie sur JavaCV, il est utile de préciser que cette bibliothèque repose sur FFmpeg pour la capture d’images vidéo — un fonctionnement similaire à celui d’OpenCV. Cependant, sous Android, OpenCV est partiellement limité : il s’appuie sur les bibliothèques multimédia du système et ne prend pas totalement en charge la lecture de flux réseau.
Transmission sur le réseau
Les protocoles RTSP (Real Time Streaming Protocol) et RTP (Real-time Transport Protocol), utilisés au-dessus d’UDP, permettent la transmission simultanée de données vidéo et audio. Ils offrent la possibilité d’accéder à distance à des caméras ou à des sources vidéo afin de récupérer des informations en temps réel, incluant à la fois l’image et le son.
La vidéo sous Android
Dans notre cas, nous allons principalement nous concentrer sur la partie vidéo — la succession d’images. Nous transformerons un flux vidéo en images individuelles et nous intéresserons à leur récupération sous forme de Bitmap.
Un Bitmap représente une image stockée en mémoire, manipulable ou affichable dans l’interface utilisateur.
Il est également possible d’afficher directement les images sur une Surface, un composant Android servant de cible de rendu vidéo, mais l’objectif ici sera de produire des Bitmaps exploitables pour le traitement d’image.
La base du code
Avant d’aborder la mise en œuvre de l’extraction d’images, voyons d’abord les permissions nécessaires et la structure de base de notre application.
Le code complet est disponible sur GitHub ici.
Permissions
Pour accéder aux fichiers vidéo, l’application a besoin des permissions suivantes :
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />Classe VideoStreamReader
Créons une classe VideoStreamReader, qui définira deux méthodes principales : start() et stop().
abstract class VideoStreamReader {
val observerList = mutableListOf<VideoObserver>()
abstract fun start(uri: Uri)
abstract fun stop()
fun notifyObservers(bitmap: Bitmap) {
observerList.forEach { it.onFrameReceived(bitmap) }
}
fun addObserver(observer: VideoObserver) {
observerList.add(observer)
}
fun removeObserver(observer: VideoObserver) {
observerList.remove(observer)
}
}Cette classe définit les méthodes essentielles qui permettront de contrôler la lecture et la récupération des images depuis un flux vidéo.
Interface VideoObserver
Afin d’être notifié lorsqu’une nouvelle image est disponible, nous mettons en place un VideoObserver :
interface VideoObserver {
fun onFrameReceived(bitmap: Bitmap)
}Avec cette base, nous pouvons désormais aborder deux implémentations concrètes : JavaCV et Media3 ExoPlayer.
Utiliser FFmpeg (JavaCV) pour lire des vidéos
Configuration
Avant d’écrire le code, il faut ajuster la configuration Gradle. Dans la section android du fichier build.gradle.kts :
packaging {
resources {
excludes += "META-INF/**"
}
}Dans AndroidManifest.xml, ajoutez la ligne suivante à l’intérieur de la balise application :
android:extractNativeLibs="true"Enfin, ajoutez les dépendances nécessaires dans la section dependencies du fichier build.gradle.kts au niveau du module :
implementation("org.bytedeco:javacpp:1.5.11")
implementation("org.bytedeco:javacv:1.5.11")
implementation("org.bytedeco:ffmpeg:7.1-1.5.11")
implementation("org.bytedeco:ffmpeg:7.1-1.5.11:android-arm64")
implementation("org.bytedeco:ffmpeg:7.1-1.5.11:android-x86_64")Classe FFmpegVideoStreamReader
Pour extraire les images, on utilise un FFmpegFrameGrabber, chargé de lire le flux vidéo. Après avoir fourni le chemin ou l’URL, il suffit de boucler sur les images via la fonction grabImage().
JavaCV met à disposition un AndroidFrameConverter pour convertir chaque Frame en Bitmap Android.
public class FFmpegVideoStreamReader extends VideoStreamReader {
private final AtomicBoolean isRunning = new AtomicBoolean(false);
@Override
public void start(@NotNull Uri uri) {
stop();
isRunning.set(true);
new Thread(() -> {
try (
FFmpegFrameGrabber ffmpegFrameGrabber = new FFmpegFrameGrabber(uri.getPath());
AndroidFrameConverter androidFrameConverter = new AndroidFrameConverter()
) {
ffmpegFrameGrabber.start();
while (isRunning.get()) {
try {
Frame frame = ffmpegFrameGrabber.grabImage();
if (frame == null) {
ffmpegFrameGrabber.setFrameNumber(0);
} else {
Bitmap bitmap = androidFrameConverter.convert(frame);
notifyObservers(bitmap);
frame.close();
}
} catch (FrameGrabber.Exception e) {
Log.e("FFmpegVideoStreamReader", e.toString());
}
}
} catch (FrameGrabber.Exception e) {
Log.e("FFmpegVideoStreamReader", e.toString());
}
}).start();
}
@Override
public void stop() {
isRunning.set(false);
}
}
Utiliser Media3 ExoPlayer pour lire des vidéos
Configuration
L’ajout de Media3 ExoPlayer est beaucoup plus simple que celui de JavaCV. Il suffit d’ajouter les dépendances suivantes dans le build.gradle.kts de votre module :
implementation("androidx.media3:media3-exoplayer:1.8.0")
implementation("androidx.media3:media3-exoplayer-dash:1.8.0")
implementation("androidx.media3:media3-ui:1.8.0")
implementation("androidx.media3:media3-ui-compose:1.8.0")
implementation("androidx.media3:media3-effect:1.8.0")Pour activer le support du RTSP et du RTP/UDP :
implementation("androidx.media3:media3-exoplayer-rtsp:1.8.0")À ce jour, Media3 prend en charge le H.264 pour le RTSP, et le support du MJPEG est toujours en cours de développement. Pour plus de détails, vous pouvez consulter les évolutions ici.
Classe Media3VideoStreamReader
Une fois Media3 configuré, nous pouvons créer notre Media3VideoStreamReader, un peu plus complexe que celui utilisant JavaCV.
const val OUTPUT_WIDTH = 1920
const val OUTPUT_HEIGHT = 1080
@UnstableApi
class Media3VideoStreamReader(context: Context) : VideoStreamReader() {
private val player = ExoPlayer.Builder(context).build()
private val imageReader =
ImageReader.newInstance(OUTPUT_WIDTH, OUTPUT_HEIGHT, PixelFormat.RGBA_8888, 2)
private val executorService = Executors.newSingleThreadExecutor()
private val videoFrameProcessorFactory =
DefaultVideoFrameProcessor.Factory.Builder().setExecutorService(
executorService
).setGlObjectsProvider(DefaultGlObjectsProvider()).build()
private var videoFrameProcessor: DefaultVideoFrameProcessor? = null
init {
player.repeatMode = Player.REPEAT_MODE_ONE
player.addListener(
object : Player.Listener {
override fun onTracksChanged(tracks: Tracks) {
tracks.groups.forEach { group ->
if (group.type == C.TRACK_TYPE_VIDEO) {
for (i in 0 until group.length) {
val format = group.getTrackFormat(i)
try {
player.clearVideoSurface()
videoFrameProcessor?.release()
videoFrameProcessor = videoFrameProcessorFactory.create(
context,
DebugViewProvider.NONE,
ColorInfo.SDR_BT709_LIMITED,
true,
executorService,
object : VideoFrameProcessor.Listener {}
).also { videoFrameProcessor ->
videoFrameProcessor.setOnInputSurfaceReadyListener {
videoFrameProcessor.setOutputSurfaceInfo(
SurfaceInfo(
imageReader.surface,
OUTPUT_WIDTH,
OUTPUT_HEIGHT
)
)
videoFrameProcessor.registerInputStream(
VideoFrameProcessor.INPUT_TYPE_SURFACE_AUTOMATIC_FRAME_REGISTRATION,
format.buildUpon()
.setColorInfo(ColorInfo.SDR_BT709_LIMITED)
.build(),
mutableListOf(),
0
)
player.setVideoSurface(videoFrameProcessor.inputSurface)
player.play()
}
}
} catch (e: Exception) {
Log.e("Media3VideoStreamReader", e.toString())
}
}
}
}
}
}
)
}
private fun acquireLatestImage(reader: ImageReader) =
reader.acquireLatestImage().let { image ->
if (image == null) return@let createBitmap(1, 1)
val planes = image.planes
val buffer = planes.first().buffer
val width = image.width
val height = image.height
val imageBuffer = buffer.rewind()
val pixelStride = planes.first().pixelStride
val rowStride = planes.first().rowStride
val rowPadding = rowStride - (pixelStride * width)
createBitmap((rowPadding / pixelStride) + width, height).let { bitmap ->
bitmap.copyPixelsFromBuffer(imageBuffer)
image.close()
bitmap
}
}
override fun start(uri: Uri) {
stop()
imageReader.setOnImageAvailableListener({ reader ->
notifyObservers(acquireLatestImage(reader))
}, Handler(Looper.getMainLooper()))
player.setMediaItem(MediaItem.fromUri(uri))
player.prepare()
}
override fun stop() {
player.stop()
}
}Cette implémentation met en place un VideoFrameProcessor pour rediriger la Surface de sortie du lecteur vers une surface gérée par un ImageReader.
Grâce à ImageReader, il devient possible de convertir la sortie vidéo en Bitmap via la fonction acquireLatestImage().
Cette approche permet de contourner le format natif d’ExoPlayer, non directement convertible, tout en garantissant une conversion fiable et performante.

Conclusion et comparaison
Avec ces deux méthodes, vous pouvez désormais extraire des images depuis une large variété de flux et les intégrer dans vos applications Android natives.
Il est néanmoins essentiel de comparer les deux approches afin de choisir celle qui correspond le mieux à vos besoins.
JavaCV offre une compatibilité étendue et fonctionne avec de nombreux formats, mais il peut rencontrer des problèmes de libération mémoire lors d’une exécution prolongée. De plus, le contrôle de la vitesse de lecture y est limité.
À l’inverse, Media3 ExoPlayer propose une solution plus stable et mieux intégrée à l’écosystème Android, au prix d’une compatibilité plus restreinte.
Cela conclut cet article. J’espère qu’il vous aura été utile et instructif, et j’ai hâte de partager davantage dans le prochain.
