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.