Lecture gapless et maux de tête avec Android et son MediaPlayer
Meh…
Je viens probablement de tomber sur le bout de code le plus étrange de l’histoire des bouts de codes. Et c’est d’autant plus étrange que ça nous vient de Google.
Incipit
Dans un lecteur de musique bien conçu, le passage d’une musique à l’autre se fait sans temps mort. On appelle ça la lecture gapless. La manière de gérer une transition fluide est généralement la même : on a un premier buffer qui lit la musique en cours et un second, qui commence à temporiser lorsque le premier arrive vers la fin. Ça, c’est la théorie.
Dans l’API Android, la classe qui permet de lire des médias audios ou vidéos s’appelle
MediaPlayer
et s’utilise comme ça :
// On récupère l'URI du média Uri myUri = .... // On crée notre lecteur MediaPlayer mediaPlayer = new MediaPlayer() // On précise que notre type d'audio est un flux de musique mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC) // On initialise notre fichier source mediaPlayer.setDataSource(getApplicationContext(), myUri) // On prépare notre lecteur mediaPlayer.prepare() // On lit la musique mediaPlayer.start()
Jusque là, rien de bien compliqué. Sauf que ça fonctionne pour une seule musique. Dès que la lecture arrive à la fin, il faut charger l’audio suivant et ça se fait comme ça :
mediaPlayer.setOnCompletionListener( new OnCompletionListener(){ @Override void onCompletion(MediaPlayer mediaPlayer) { // Rebelotte mediaPlayer.reset() mediaPlayer.setDataSource(getApplicationContext(), myUri); mediaPlayer.prepare(); mediaPlayer.start(); } });
Sauf que vous voyez où je veux en venir : avec une telle méthode, ça ne fait pas de lecture gapless. Lorsque la musique arrive à la fin, le
MediaPlayer
doit être réinitialisé et redémarré pour lire une nouvelle musique.
Depuis Android 4.1, cependant (soit l’API niveau 16), il est possible de faire un enchaînement fluide grâce à la méthode
void setNextMediaPlayer (MediaPlayer next)
de la classe. Il nous faut donc un second MediaPlayer pour assurer le fondu.
Le mal de tête
Voilà comment je m’y suis pris au départ : ma classe de gestion de la lecture possède deux
MediaPlayer
. Le premier devait me servir à la musique courante et le second, à la musique suivante. Lorsque la musique suivante arrivait à sa fin, j’avais prévu d’échanger les deux
MediaPlayer
, de réinitialiser celui qui gère la musique suivante, puis de le réaffecter avec la méthode
setNextMediaPlayer()
. Voici mon code originel (je précise que c’est du Groovy et pas du Java) :
class MusicService{ private MediaPlayer mediaPlayer = new MediaPlayer() private MediaPlayer nextMediaPlayer = new MediaPlayer() // Du code ici... // Mon initialisation private void mediaPlayerInit(MediaPlayer m) { m.audioStreamType = AudioManager.STREAM_MUSIC m.setWakeMode(applicationContext, PowerManager.PARTIAL_WAKE_LOCK) m.setOnPreparedListener = this m.onCompletionListener = this m.onErrorListener = this } // Ma préparation private void mediaPlayerPrepare(MediaPlayer m , Uri uri) { m.reset() if(uri != null) { m.setDataSource(applicationContext, uri) m.prepare() } } //Ma callback @Override void onCompletion(MediaPlayer me) { mediaPlayer = nextMediaPlayer nextMediaPlayer = null mediaPlayerPrepare(nextMediaPlayer, uri) } }
Bon, je ne vais pas vous faire languir, ça ne fonctionne pas. J’ai erré un certain temps, voire un temps certain avant de trouver la solution. Le problème est que la documentation d’Android n’explique absolument pas l’effet de bord généré par
setNextMediaPlayer()
. Et il est gros : dès que la musique change, notre précédent
MediaPlayer
est automatiquement relâché. Plus moyen, donc, d’invoquer des méthodes pour obtenir le durée de la chanson ou le temps de lecture. Mais en plus, le premier et le second
MediaPlayer
sont étroitement liés : si vous réinitialisez le premier, le second le sera aussi.
La solution
Je l’ai trouvée ici et elle est totalement contre-intuitive : il faut réinitialiser les deux
MediaPlayer
. Et, contrairement à ce que je pensais, ça ne stoppe absolument pas la lecture. Voici le code complet :
class MusicService{ private MediaPlayer mediaPlayer = new MediaPlayer() private MediaPlayer nextMediaPlayer = new MediaPlayer() // Du code ici... // Mon initialisation private void mediaPlayerInit(MediaPlayer m) { m.audioStreamType = AudioManager.STREAM_MUSIC m.setWakeMode(applicationContext, PowerManager.PARTIAL_WAKE_LOCK) m.setOnPreparedListener = this m.onCompletionListener = this m.onErrorListener = this } // Ma préparation private void mediaPlayerPrepare(MediaPlayer m , Uri uri) { m.reset() if(uri != null) { m.setDataSource(applicationContext, uri) m.prepare() } } //Ma callback @Override void onCompletion(MediaPlayer me) { // Récupérer l'URI de la chanson suivante def uri = ... // Je réinitialise mon premier MediaPlayer mediaPlayerPrepare(mediaPlayer, uri) // Je prépare le suivant prepareNextPlayer() // Je relance la lecture start() } }
En espérant que ça aide quelqu’un.
Edit
En testant ma solution ce matin avec la tête un peu moins dans le cul, je me suis rendu compte que ma solution laisse quand-même un léger blanc. J’ai donc revu un peu le code de la fonction
onCompletion()
:
//Ma callback @Override void onCompletion(MediaPlayer me) { // Récupérer l'URI de la chanson suivante def uri = ... mediaPlayer = nextMediaPlayer nextMediaPlayer = new MediaPlayer() mediaPlayerPrepare(nextMediaPlayer, uri) prepareNextPlayer() }
La différence principale c’est qu’au lieu de réinitialiser les deux
MediaPlayer
j’affecte
nextMediaPlayer
à
mediaPlayer
puis je créé un nouveau
MediaPlayer
dans
nextMediaPlayer
. J’espère que le colecteur de déchets saura gérer ça.
Les commentaires sont fermés.