MediaPlayer para reproducir audios en Android Studio | Jetpack Compose

Video thumbnail

La reproducción de contenido multimedia, como audio y video, es una característica fundamental en muchas aplicaciones de Android. A lo largo de los años, las herramientas y APIs para esta tarea han evolucionado significativamente. En este artículo, exploraremos tanto el enfoque tradicional (Legacy) usando la clase MediaPlayer como el enfoque moderno y recomendado con Jetpack Compose y la biblioteca Media3 (ExoPlayer).

Anteriormente, vimos como usar LinearProgressIndicator y CircularProgressIndicator en Android Studio | Jetpack Compose

Creación de un Reproductor de Audio Simple en Android

Vamos a aprender a reproducir audios para crear nuestro propio reproductor. A continuación, te presento una demostración de lo que vamos a desarrollar: un par de botones (Play y Pause) que controlan la reproducción. Para esto, utilizaremos la clase MediaPlayer.

1. Preparación de los Recursos

Lo primero que debemos hacer es preparar nuestro archivo de audio:

  1. Ve a la carpeta res (Resources).
  2. Crea una subcarpeta llamada raw (aunque el nombre es opcional, es el estándar recomendado para archivos multimedia).
  3. Copia cualquier archivo de audio que tengas (por ejemplo, music.mp3). Te sugiero usar un nombre sencillo para facilitar el acceso al recurso.

2. Implementación del Composable

En nuestro método onCreate, vamos a crear un nuevo Composable llamado ReproductorSimple. Aquí es donde necesitaremos el Contexto, que representa el ámbito de nuestra actividad.

@Composable
fun ReproductorSimple() {
    val context = LocalContext.current

    // 1. Creamos y recordamos el MediaPlayer
    val mediaPlayer = remember {
        MediaPlayer.create(context, R.raw.music)
    }

    // 2. Controlamos la limpieza al salir de la pantalla
    DisposableEffect(Unit) {

        onDispose {
            mediaPlayer.stop()
            mediaPlayer.release() // Libera la memoria
        }
    }

    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Row {
            Button(onClick = { mediaPlayer.start() }) {
                Icon(Icons.Default.PlayArrow, contentDescription = null)
                Text("Play")
            }
            Spacer(Modifier.width(8.dp))
            Button(onClick = { if (mediaPlayer.isPlaying) mediaPlayer.pause() }) {
                Text("Pause")
            }
        }
    }
}

3. Explicación Técnica y Gestión de Memoria

La clase R (Resources)

Es la primera vez que utilizamos la clase R. Es una clase especial en Android que nos permite acceder a los recursos del proyecto de forma sencilla (por ejemplo, R.raw.music), evitando tener que cargar archivos mediante rutas de texto o librerías externas complejas.

El uso de DisposableEffect

Este es un punto clave para evitar fugas de memoria. En Android, si cerramos la Actividad (la página actual) pero no liberamos el audio, este podría quedarse cargado en la memoria del dispositivo.

DisposableEffect es un mecanismo de Jetpack Compose para gestionar efectos secundarios que deben limpiarse cuando el Composable deja de mostrarse.

Dentro del bloque onDispose, detenemos (stop()) y liberamos (release()) el MediaPlayer.

4. Ciclo de Vida en Jetpack Compose vs. Legacy

Antiguamente (en el esquema basado en XML o Legacy), utilizábamos métodos globales de la actividad como onPause, onStop o onDestroy para liberar recursos.

Sin embargo, Jetpack Compose (el framework moderno de interfaz gráfica para Android anunciado en 2019) permite manejar esto de forma local. Con DisposableEffect, no necesitamos ensuciar los métodos globales de la actividad; podemos gestionar la limpieza de recursos (como reproductores u observadores) directamente donde se usan.

Reproducir desde una URL (Streaming)

Cuando el audio viene de internet, el proceso es asíncrono porque no sabemos cuánto tardará en descargar.

@Composable
fun ReproductorOnline(url: String) {
   val context = LocalContext.current
   val mediaPlayer = remember { MediaPlayer() }
   var isReady by remember { mutableStateOf(false) }
   LaunchedEffect(url) {
       mediaPlayer.apply {
           setAudioAttributes(
               AudioAttributes.Builder()
                   .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                   .setUsage(AudioAttributes.USAGE_MEDIA)
                   .build()
           )
           setDataSource(url)
           // Usamos prepareAsync para no bloquear la interfaz
           prepareAsync() 
           setOnPreparedListener {
               isReady = true
           }
       }
   }
   Button(
       onClick = { mediaPlayer.start() },
       enabled = isReady // El botón se activa solo cuando cargó
   ) {
       Text(if (isReady) "Reproducir Online" else "Cargando...")
   }
}

Consejos "Pro" para MediaPlayer:

  • Wake Lock: Si vas a reproducir música larga con la pantalla apagada, necesitas permisos especiales para que el procesador no se duerma.
  • Audio Focus: Es buena práctica detener tu música si entra una llamada o si el usuario abre YouTube.
  • Memoria: ¡Nunca olvides el mediaPlayer.release()! Si creas muchos sin liberarlos, tu app se quedará sin memoria rápidamente.

El enfoque Moderno: Jetpack Compose y Media3 (ExoPlayer)

Con la llegada de Jetpack Compose y las librerías de AndroidX, la forma recomendada de trabajar con multimedia es a través de Jetpack Media3. Esta es una colección de librerías de soporte que incluye a ExoPlayer, un reproductor multimedia a nivel de aplicación que es mucho más robusto, flexible y fácil de usar que el antiguo MediaPlayer.

1. Añadir las dependencias

Primero, asegúrate de tener las dependencias de androidx.media3 en el fichero build.gradle de tu módulo (app).

dependencies {
    // ... otras dependencias
    def media3_version = "1.3.1"

    implementation "androidx.media3:media3-exoplayer:$media3_version"
    // Opcional: para componentes de UI
    implementation "androidx.media3:media3-ui:$media3_version"
    // Opcional: para reproducción en segundo plano y notificaciones
    implementation "androidx.media3:media3-session:$media3_version"
}

2. Crear y gestionar ExoPlayer en un Composable

La forma idiomática de usar ExoPlayer en Jetpack Compose es crearlo y gestionarlo dentro de una función Composable, manejando su ciclo de vida con remember y DisposableEffect.

import android.net.Uri
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.LifecycleOwner
import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer

@Composable
fun AudioPlayerComposable(
    audioUrl: String,
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current
) {
    val context = LocalContext.current

    // 1. Inicializamos ExoPlayer
    val exoPlayer = remember {
        ExoPlayer.Builder(context).build().apply {
            // 2. Creamos un MediaItem desde la URL
            val mediaItem = MediaItem.fromUri(Uri.parse(audioUrl))
            setMediaItem(mediaItem)
            
            // 3. Preparamos el reproductor
            prepare()
            
            // Inicia la reproducción automáticamente cuando esté listo
            playWhenReady = true
        }
    }

    // 4. Gestionamos el ciclo de vida del reproductor
    DisposableEffect(lifecycleOwner) {
        onDispose {
            // Liberamos el reproductor cuando el Composable se va
            exoPlayer.release()
        }
    }

    // Aquí podrías añadir controles de UI (Play/Pause, Seekbar) 
    // que interactúen con la instancia de `exoPlayer`.
}

Explicación del código moderno:

  • remember { ... }: Asegura que la instancia de ExoPlayer se cree solo una vez y se conserve a través de las recomposiciones del Composable.
  • ExoPlayer.Builder(context).build(): Es la forma actual de construir una instancia de ExoPlayer.
  • MediaItem.fromUri(...): Media3 usa MediaItem para representar el contenido multimedia. Es más flexible que el antiguo setDataSource.
  • prepare(): Al igual que antes, prepara el reproductor. ExoPlayer maneja la preparación de forma asíncrona por defecto, por lo que no hay riesgo de bloquear el hilo principal.
  • DisposableEffect: Es crucial para la limpieza. El bloque onDispose se ejecuta cuando el Composable es eliminado del árbol de composición, asegurando que liberemos los recursos de ExoPlayer para evitar fugas de memoria.

El enfoque Legacy: MediaPlayer

Como su nombre indica, los MediaPlayer son empleados para reproducir y controlar la reproducción de audios y/o videos en Android. Aunque su uso ha sido reemplazado por librerías más modernas, es importante entender su funcionamiento, ya que aún se encuentra en mucho código existente.

Para reproducir un audio primero debemos crear un objeto de la clase MediaPlayer:

player = new MediaPlayer();

Ahora debemos de indicar la fuente de los datos, los cuales pueden ser internos, es decir localizados en el mismo dispositivo Android o en una web y la accedemos vía una URL como la siguiente:

player.setDataSource("http://example.net/uploads/fuente/"
+ cancion.getFuente());

Android se encarga de descargar el audio; una vez establecido el fuente preparamos el audio e iniciamos la reproducción:

player.prepare();
player.start();

Finalmente nuestro código queda de la siguiente forma:

try {
    player.setDataSource("http://example.net/uploads/fuente/"
    + cancion.getFuente());
    player.prepare();
    player.start();
} catch (Exception e) {
    e.printStackTrace();
}

Audios cargados de manera asíncrona

Dependiendo del propósito de su aplicación puede que te de algunos problemas el código mostrado anteriormente, la razón se basa en que los audios hasta que no estén completamente preparado prepare() para su reproducción la aplicación se nos puede "colgar" dependiendo en que hilo en donde invoquemos dicho método que generalmente es en hilo principal y es posible que aparezca la famosa ventana de "No responde":

No responde app android

En la cual nos indica que la aplicación no responde y si deseas cerrarla; cosa que no es positiva en la experiencia que tenga el usuario con nuestra aplicación.

prepareAsync para preparar el audio de manera asíncrona

El problema se hace mayor si por ejemplo todos los audios nos los traemos desde Internet y la aplicación consiste en un listado de audios, y si al usuario empieza a seleccionar varios de ellos el cuelgue de nuestra aplicación se hace inminente...

La solución es realmente sencilla y no tenemos que crear servicios, hilos, etc y colocar todo el procesamiento dentro de los mismos ni nada por el estilo; simplemente tenemos que emplear el método prepareAsync y cambiar unas pequeñas cosas del código anterior y agregar otras.

Primero el método prepareAsync permite preparar al audio igual que el método prepare pero lo realiza de manera asíncrona, por lo tanto el audio deberá reproducirse (dependiendo de la acción que queramos hacer sobre el mismo) una vez que el audio esté listo o preparado; para eso empleamos un evento escuchador onPrepared y dentro del mismo movemos nuestro método play():

try {
    player.setDataSource("http://example.net/uploads/fuente/"
    + cancion.getFuente());
    player.setOnPreparedListener(this);
    player.prepareAsync();
} catch (Exception e) {
    e.printStackTrace();
}

Y definimos nuestro evento escuchador quedando de la siguiente forma:

@Override
public void onPrepared(MediaPlayer mediaPlayer) {
    if (mediaPlayer.isPlaying()) {
        mediaPlayer.pause();
    }
    mediaPlayer.start();
}

Y con esto adiós cuelgue (al menos los ocasionados por el método prepare); como vemos, con el método prepareAsync el cual no necesita ser definido en un hilo o proceso aparte y es Android el que se encarga del fuerte de manejar los procesos en segundo plano necesarios.

Caso práctico: Realizando un pequeño reproductor

Un enfoque que nos permite emplear los MediaPlayer para la reproducción paralela de recursos multimedias como audios y/o videos es definir una clase aparte que extienda de los servicios, de esta forma podemos tener una clase madre que será nuestra actividad que invoque (y controle según necesidades) al servicio, además de esto el servicio permitirá realizar una serie de funcionalidades mediante métodos predefinidos o ya definidos en otras clases que conforman la API, y esto se hace mediante la implementación a estas clases (OnPreparedListener,OnErrorListener y OnCompletionListener).

Crearemos una clase que para este ejemplo llamaremos MusicService:

public class MusicService extends Service implements MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener { }

Ahora definimos unas variables globales que nos permitirán realizar el funcionamiento de una lista de canciones; además sobreescribimos los métodos correspondientes de acuerdo a las clases heredades e implementadas:

public class MusicService  extends Service implements
MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener,
MediaPlayer.OnCompletionListener {

    //media player
    private MediaPlayer player;
    //song list
    private ArrayList<Cancion> canciones;
    private Cancion cancion;
    //current position
    private int songPosn;

    private final IBinder musicBind = new MusicBinder();

    @Override
    public void onCreate() {
        super.onCreate();
        initMusicPlayer();
    }

    @Override
    public IBinder onBind(Intent intent) {
        return musicBind;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        Bundle extras = intent.getExtras();
        int accion = 0;
        if (extras != NULL) {
            accion = extras.getInt("accion");
        }

        return START_NOT_STICKY;
    }

    @Override
    public boolean onUnbind(Intent intent) {
        try {
            player.stop();
            player.release();
        } catch (Exception e) {
            Log.e("ERROR", e.toString());
        }
        return false;
    }

    @Override
    public boolean onError(MediaPlayer mp, int what, int extra) {
        mp.reset();
        return false;
    }

    @Override
    public void onCompletion(MediaPlayer mp) {
        mp.reset();
    }

    @Override
    public void onPrepared(MediaPlayer mediaPlayer) {
        if(mediaPlayer.isPlaying()) {
            mediaPlayer.pause();
        }

        mediaPlayer.start();
    }
}

Como vemos, sobreescribimos varios métodos que se ejecutan en el ciclo de vida de un video o audio, como son si ocurre algún error en la reproducción mediante el método onError, cuando se completó la reproducción de la música mediante el método onCompletion, cuando el video o música está listo para su reproducción mediante el método onPrepared.

También tenemos algunos métodos del servicio como el onUnbind que se invoca cuando el cliente se conecta con el servicio o el método onStartCommand que se invoca cuando la actividad inicia el servicio; con esto definimos otros métodos que nos permitirán automatizar la reproducción de nuestra música que empleamos en los métodos sobrescritos por las clases implementadas anteriores:

public class MusicService  extends Service implements
MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener,
MediaPlayer.OnCompletionListener {

    private MediaPlayer player;

    private ArrayList<Cancion> canciones;
    private Cancion cancion;

    private int songPosn;

    private final IBinder musicBind = new MusicBinder();

    @Override
    public void onCreate() {
        super.onCreate();
        initMusicPlayer();
    }

    @Override
    public IBinder onBind(Intent intent) {
        return musicBind;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        Bundle extras = intent.getExtras();
        int accion = 0;
        if (extras != NULL) {
            accion = extras.getInt("accion");

            switch (accion) {
                case 1:
                    if (isPng()) {
                    pausePlayer();
                } else {
                    go();
                }
                break;
                case 3:
                    playNext();
                break;
                case 4:
                    playPrev();
                break;
            }
        }

        return START_NOT_STICKY;
    }

    @Override
    public boolean onUnbind(Intent intent) {
        try {
            player.stop();
            player.release();
        } catch (Exception e) {
            Log.e("ERROR", e.toString());
        }
        return false;
    }

    @Override
    public boolean onError(MediaPlayer mp, int what, int extra) {
        mp.reset();
        return false;
    }

    @Override
    public void onCompletion(MediaPlayer mp) {
        mp.reset();
        playNext();
    }

    @Override
    public void onPrepared(MediaPlayer mediaPlayer) {
        if(mediaPlayer.isPlaying()) {
            mediaPlayer.pause();
        }

        mediaPlayer.start();
    }

    public void initMusicPlayer() {
        songPosn = 0;
        player = new MediaPlayer();
        player.setWakeMode(getApplicationContext(),
        PowerManager.PARTIAL_WAKE_LOCK);
        player.setAudioStreamType(AudioManager.STREAM_MUSIC);
        player.setOnPreparedListener(this);
        player.setOnCompletionListener(this);
        player.setOnErrorListener(this);
    }

    public void setList(ArrayList<Cancion> canciones) {
        this.canciones = canciones;
    }

    public void playSong() {

        if (songPosn >= canciones.size())
        return;

        try {
            player.reset();
        } catch (Exception e) {
            Log.i("Error", e.toString());
        }

        cancion = canciones.get(songPosn);

        try {
            player.setDataSource("http://url/"
            + cancion.getFuente());
            player.setOnPreparedListener(this);
            player.prepareAsync();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            Log.e("MUSIC SERVICE", "Error setting data source", e);
        }
    }

    public class MusicBinder extends Binder {
        public MusicBinder() {
        }

        public MusicService  getService() {
            return MusicService .this;
        }
    }

    public int getPosn() {
        return player.getCurrentPosition();
    }

    public int getDur() {
        return player.getDuration();
    }

    public boolean isPng() {
        return player.isPlaying();
    }

    public void pausePlayer() {
        player.pause();
    }

    public void seek(int posn) {
        player.seekTo(posn);
    }

    public void go() {
        player.start();
    }

    public void playPrev() {
        songPosn--;
        if (songPosn < 0) songPosn = canciones.size() - 1;
        playSong();
    }

    //skip to next
    public void playNext() {

        songPosn++;
        if (songPosn >= canciones.size()) songPosn = 0;

        playSong();
    }

    @Override
    public void onDestroy() {
        stopForeground(true);
    }
}

Ya con esto tendríamos el servicio completo, se crearon algunos métodos que permiten verificar el estatus de la reproducción del audio, pasar a la siguiente música, iniciar la reproducción, colocar en pausa, etc.

Ahora, como indicamos en un inicio, el servicio es iniciado desde otro componente que en este caso representa a una actividad; para iniciar el servicio anterior desde una actividad tenemos:

public MusicService musicSrv;
public boolean musicBound = false;

private ServiceConnection musicConnection = new ServiceConnection() {

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        MusicService.MusicBinder binder = (MusicService.MusicBinder) service;
        //get service
        musicSrv = binder.getService();
        //pass list
        musicSrv.setList(canciones);
        musicBound = true;

        current = 0;
        try {
            songPicked(NULL);
        } catch (Exception e) {
            Log.e("ERROR SERVICE", e.toString());
        }

    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
        musicBound = false;
        Log.i("onServiceDisconnected", "Desconectado");
    }
};

Para mandar si deseamos reproducir, pausar, pasar al siguiente audio desde la actividad que invocó el servicio o cualquier otra comunicación que deba ocurrir entre la actividad y el servicio tenemos:

private void playNext() {
    musicSrv.playNext();
    current++;
    songPicked();
}

private void playPrev() {
    musicSrv.playPrev();
    current--;
    songPicked();
}

Quedaría es asociar esas funciones a los botones u otro elemento para su acción.

Canciones es una clase modelo con la siguiente estructura:

public class Cancion {

    int cancion_id;
    String fuente;
    String titulo;
    String anombre;
    String imagen_url;

    public String getAnombre() {
        return anombre;
    }

    public void setAnombre(String anombre) {
        this.anombre = anombre;
    }
    public int getCancion_id() {
        return cancion_id;
    }

    public void setCancion_id(int cancion_id) {
        this.cancion_id = cancion_id;
    }

    public String getFuente() {
        return fuente;
    }

    public void setFuente(String fuente) {
        this.fuente = fuente;
    }

    public String getTitulo() {
        return titulo;
    }

    public void setTitulo(String titulo) {
        this.titulo = titulo;
    }

    public String getImagen_url() {
        return imagen_url;
    }

    public void setImagen_url(String imagen_url) {
        this.imagen_url = imagen_url;
    }
}

Conclusión

Aunque MediaPlayer fue la base para la reproducción de multimedia en Android durante años, el ecosistema ha evolucionado. Jetpack Media3 y ExoPlayer ofrecen una API más potente, flexible y fácil de mantener, especialmente cuando se combina con la naturaleza declarativa de Jetpack Compose. Para nuevos desarrollos, este es sin duda el camino a seguir.

El siguiente paso consiste en aprender a usar el Acelerómetro en Android Studio.

Aprende a crear un reproductor de audio en Android con Jetpack Compose y MediaPlayer. Guía paso a paso sobre gestión de recursos en la carpeta RAW, control de reproducción y manejo de memoria con DisposableEffect.

Acepto recibir anuncios de interes sobre este Blog.

Andrés Cruz

EN In english