Photo by ROBIN WORRALL on Unsplash
Lecture de contenu multimédia dans une application Android avec ExoPlayer et Chromecast
Les applications mobiles font maintenant partie de notre quotidien quant à la consommation de contenus multimédias. Netflix, YouTube, Spotify... sont autant d’acteurs qui ont redéfini notre manière de regarder des vidéos ou encore d’écouter de la musique avec nos téléphones.
Dans cet article, nous allons nous intéresser à la mise en place, de manière basique, de la lecture de vidéos dans une application Android grâce à une bibliothèque dédiée : ExoPlayer. Nous verrons également comment permettre la diffusion du contenu sur grand écran via Chromecast.
ExoPlayer
ExoPlayer est une bibliothèque open-source créée par Google (et utilisée pour la lecture de Youtube). Elle propose une solution flexible, puissante et facile à intégrer pour la lecture de contenus multimédias dans les applications Android.
ExoPlayer permet notamment :
Une prise en charge étendue des formats audio et vidéo (MP4, WebM, MKV...) ainsi que de leurs codecs
Une gestion profonde et adaptative de la lecture en streaming via la gestion d'une multitude de procotoles tels que DASH, HLS, SmoothStreaming...
Une personnalisation avancée via des composants que les développeurs peuvent utiliser selon leurs besoins.
La lecture de contenu protégé par le DRM (Digital Rights Management)
Mise en place dans votre application Android
Pour commencer il vous faut importer les dépendances dans le build.gradle
de votre application
implementation "androidx.media3:media3-exoplayer:1.2.0"
implementation "androidx.media3:media3-exoplayer-dash:1.2.0"
implementation "androidx.media3:media3-ui:1.2.0"
La bibliothèque ExoPlayer propose un éventail de composants UI pour la lecture de contenus multimédias. Nous allons ici utiliser le composant PlayerView
dans le fichier XML de la vue du Fragment ou de l’Activité
<androidx.media3.ui.PlayerView
android:id="@+id/player_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
Une fois le PlayerView
créé, nous pouvons passer du côté de la classe de l'Activité / du Fragment pour initialiser l'objet ExoPlayer
, qui va nous permettre de gérer et customiser le comportement du lecteur vidéo, et le lier à la PlayerView
.
val player: ExoPlayer = ExoPlayer.Builder(this).build()
playerView.player = player
Vient ensuite la création du MediaItem
. C'est la représentation abstraite de l'élément multimédia que l'on souhaite lire. Il permet d'encapsuler les informations du média, telle que l'URI de la ressource, les métadonnées, les sous-titres, etc. Dans notre exemple, nous allons essayer de lire une vidéo en pointant vers son URL.
Enfin, on ajoute le MediaItem
à l'ExoPlayer
, et on lui indique de charger les informations du contenu multimédia pour être prêt pour la lecture avec la méthode .prepare()
val mediaItem = MediaItem.Builder()
.setUri("https://example.com/sample.mp4")
.setMimeType(MimeTypes.VIDEO_MP4)
.build()
player.setMediaItem(mediaItem)
player.prepare()
Il est important de ne pas oublier de libérer la ressource du player une fois qu'on en a plus besoin, afin d'éviter de créer des fuites mémoire, qui pourraient impacter les performances et la stabilité de l'application.
override fun onDestroy() {
player.release()
super.onDestroy()
}
TUDUUM TADAAA vous avez une application qui est capable de lire une vidéo. La configuration de cet exemple reste basique, mais elle démontre que l'intégration de la bibliothèque ExoPlayer est rapide et efficace.
Chromecast
La plupart des applications de lecture de contenus vidéo propose de diffuser le contenu sur de plus grands écrans, comme les télévisions, notamment via la technologie Chromecast.
Pour pouvoir ajouter cette fonctionnalité à notre petite application, nous allons devoir intégrer le SDK Google Cast.
Avant de se lancer dans l'intégration, il faut noter que pour lire le contenu sur un Chromecast nous avons besoin de trois choses :
Un appareil Chromecast
Une application dite "sender" : c'est l'application qui doit envoyer les informations du média au Chromecast (dans notre cas c'est notre application)
Une application dite "receiver" : c'est l'application qui va s'exécuter sur le Chromecast et qui va lire le contenu que l'application "sender" envoie.
Concernant l'application "receiver" il est possible, et même recommandé, d'implémenter une application custom (en JS/HTML par exemple) afin de pouvoir customiser le player affiché dans le Chromecast.
Afin de ne pas complexifier notre exemple, nous allons utiliser une application receiver web par défaut proposée par Google qui ne permet pas de customisation de l'interface du player.
Pour intégrer le SDK Google Cast, on doit commencer par importer la librairie dans le build.gradle
implementation("com.google.android.gms:play-services-cast-framework:21.4.0")
Les interactions avec le SDK Google Cast se font via un objet Singleton, le CastContext
. Pour initialiser cet objet, nous avons besoin de fournir des informations au SDK, via l'implémentation de l'interface OptionsProvider.
Nous avons notamment besoin de l'identifiant de l'application "receiver", ici nous utiliserons CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID
qui est l'identifiant de l'application par défaut proposée par Google
class CastOptionsProvider : OptionsProvider {
override fun getCastOptions(context: Context): CastOptions {
return CastOptions.Builder()
.setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID)
.build()
}
override fun getAdditionalSessionProviders(context: Context): MutableList<SessionProvider>? {
return null
}
}
Une fois cette implémentation effectuée, nous devons la déclarer dans le AndroidManifest.xml
dans un champ métadonnées
<meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="com.example.nijitechexoplayer.cast.CastOptionsProvider" />
Vient ensuite l'étape d'ajouter un bouton pour déclencher le processus de cast. Pour ce faire, plusieurs choix s'offrent à nous. Nous pouvons utiliser le bouton fourni par le SDK, le MediaRouteButton, ou alors nous pouvons utiliser un élément dans un menu.
Le choix se fait en fonction des besoins, par exemple, si vous voulez placer le bouton dans le Player, avec les autres éléments de contrôle (play, pause, avance rapide...) cela nécessite de customiser l'interface des contrôles du Player, et d'utiliser le MediaRouteButton. Dans cet exemple, le choix a été fait d'utiliser un élément dans un menu.
Pour ajouter le bouton à un menu, on crée un fichier menu.xml
dans res/menu
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/media_route_menu_item"
android:title="@string/media_notification_channel_name"
app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
app:showAsAction="always" />
</menu>
Puis on va lier notre bouton au SDK Google Cast via la classe CastButtonFactory
.
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.menu, menu)
CastButtonFactory.setUpMediaRouteButton(
applicationContext,
menu,
R.id.media_route_menu_item
)
return true
}
Il est maintenant temps de retourner dans notre Activity, et de commencer à gérer les sessions de cast. Pour ce faire, nous allons utiliser certains outils fournis par le SDK, à savoir :
CastContext : c'est le Singleton précédemment évoqué et qui permet de gérer les interactions avec le SDK
SessionManager : c'est l'objet qui va nous permettre d'interagir avec les sessions et de configurer un listener d'évènement de session (début de session, fin de session, reprise...)
CastSession : c'est l'objet qui va représenter la session de cast actuel.
SessionManagerListener<CastSession>
: c'est le listener (implémenté dans l'activité) qui va nous permettre de suivre les événements liés à la session de cast.RemoteMediaClient.ProgressListener : c'est un listener qui va nous permettre de sauvegarder dans l'activité la progression de la vidéo lors de la lecture sur le Chromecast, afin de reprendre la lecture au même endroit dans l'application lorsque la session de cast est arrêtée.
Pour commencer, on va initialiser le CastContext
ainsi que le SessionManager
dans le onCreate()
de l'activité
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
mCastContext = CastContext.getSharedInstance(this)
mSessionManager = mCastContext.sessionManager
}
Ensuite, dans le onResume()
, on va récupérer la CastSession
en cours (si elle existe) et on va attacher le listener implémenté par l'activité au SessionManager
override fun onResume() {
super.onResume()
...
mCastSession = mSessionManager.currentCastSession
mSessionManager.addSessionManagerListener(this, CastSession::class.java)
}
Enfin, dans le onPause()
, on s'assure que le listener et la CastSession
ne sont plus stockés
override fun onPause() {
super.onPause()
mSessionManager.removeSessionManagerListener(this, CastSession::class.java)
mCastSession = null
...
}
L'implémentation de RemoteMediaClient.ProgressListener
est simple, on sauvegarde la progression dans une variable Long
override fun onProgressUpdated(position: Long, p1: Long) {
playbackPosition = position
}
Maintenant, on peut implémenter le listener d'événement de session SessionManagerListener<CastSession>
. Il y a plusieurs méthodes à implémenter, mais celles qui nous intéressent dans notre exemple sont :
onSessionEnded()
: lorsque la session de cast se termine (par exemple lorsque l'utilisateur clique sur "Arrêter la diffusion"onSessionStarted()
: lorsque la session de cast commence (après que l'utilisateur ait décidé de caster sur son appareil Chromecast). C'est notamment via cette méthode que nous allons envoyer les données du média au Chromecast
Quand la session de cast se termine, nous voulons indiquer au player dans l'application la dernière progression pour que l'utilisateur puisse reprendre la lecture à l'endroit où s'est terminée la session de cast
override fun onSessionEnded(p0: CastSession, p1: Int) {
player.seekTo(playbackPosition)
}
Pour envoyer les informations du média au Chromecast, on va se servir de la méthode onSessionStarted()
, car elle nous indique que le lien entre l'application et le Chromecast est établie et que ce dernier est prêt à recevoir les données.
On récupère d'abord la session de cast en cours, puis on met en pause le lecteur de l'application (pour ne pas avoir une double lecture Chromecast/application qui serait inutile). Enfin on construit et envoie les informations au Chromecast
override fun onSessionStarted(castSession: CastSession, p1: String) {
mCastSession = castSession
if (player.isPlaying) {
player.pause()
}
loadRemoteMedia(player.currentPosition.toInt())
}
C'est dans la méthode loadRemoteMedia()
que l'on construit les informations à envoyer au SDK Google Cast.
On va d'abord créer un objet MediaMetadata, afin de renseigner des éléments à afficher sur le player comme le titre de la vidéo, un sous-titre, ou encore des images
private fun getMediaMetaData(): MediaMetadata {
val mediaMetaData = MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE)
mediaMetaData.putString(MediaMetadata.KEY_TITLE, "Ma vidéo Niji Tech")
mediaMetaData.putString(MediaMetadata.KEY_SUBTITLE, "Mon sous-titre")
return mediaMetaData
}
Ensuite, on crée un objet MediaInfo, qui va nous permettre de renseigner les informations liées au média, comme son URL d'accès, le type de flux du média (si c'est un live, une vidéo à charger...), le type mime du contenu (vidéo mp4 dans notre cas), des metadata... etc.
En parallèle du MediaInfo, on crée un MediaLoadOptions, qui va indiquer à l'application receiver comment charger le média, dans notre cas on va juste indiquer la position à partir de laquelle lire la vidéo.
Enfin, on envoie toutes ces informations au Chromecast via le RemoteMediaClient de la session de cast en cours. Sans oublier de lier le RemoteMediaClient et notre listener pour la sauvegarde de la progression de la vidéo lues sur le Chromecast
private fun loadRemoteMedia(position: Int) {
if (mCastSession == null) {
return
}
val mediaInfo = MediaInfo.Builder(VIDEO_URL)
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
.setContentType(MimeTypes.VIDEO_MP4)
.setMetadata(getMediaMetaData())
.build()
val mediaLoadOptions = MediaLoadOptions.Builder()
.setPlayPosition(position.toLong())
.build()
val remoteMediaClient = mCastSession!!.remoteMediaClient ?: return
remoteMediaClient.addProgressListener(this, 1000)
remoteMediaClient.load(mediaInfo, mediaLoadOptions)
}
🎉 Et voilà, notre application est désormais capable de lire une vidéo grâce à ExoPlayer, mais aussi de streamer son contenu sur un Google Chromecast
Pour aller plus loin
Dans cet article, nous n'avons pas parlé de toutes les configurations que nous pouvons faire avec ExoPlayer, comme par exemple, la mise en pause du player lorsque l'application est envoyée en arrière plan et la reprise de la lecture au même moment que lorsque le player a été mis en pause lors du retour au premier plan de l'application.
Voici quelques autres points, plus avancés, qui seront peut-être abordés lors de futurs articles :
La gestion du plein écran
La lecture de contenu respectant d'autres protocoles de streaming
La customisation du lecteur vidéo
et bien d'autres encore...