Índice de contenido
- Creación de un Reproductor de Audio Simple en Android
- 1. Preparación de los Recursos
- 2. Implementación del Composable
- 3. Explicación Técnica y Gestión de Memoria
- La clase R (Resources)
- El uso de DisposableEffect
- 4. Ciclo de Vida en Jetpack Compose vs. Legacy
- Reproducir desde una URL (Streaming)
- El enfoque Moderno: Jetpack Compose y Media3 (ExoPlayer)
- 1. Añadir las dependencias
- 2. Crear y gestionar ExoPlayer en un Composable
- Explicación del código moderno:
- El enfoque Legacy: MediaPlayer
- Audios cargados de manera asíncrona
- prepareAsync para preparar el audio de manera asíncrona
- Caso práctico: Realizando un pequeño reproductor
- Conclusión
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:
- Ve a la carpeta res (Resources).
- Crea una subcarpeta llamada raw (aunque el nombre es opcional, es el estándar recomendado para archivos multimedia).
- 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
ExoPlayerse 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(...):
Media3usaMediaItempara representar el contenido multimedia. Es más flexible que el antiguosetDataSource. - prepare(): Al igual que antes, prepara el reproductor.
ExoPlayermaneja 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
onDisposese ejecuta cuando el Composable es eliminado del árbol de composición, asegurando que liberemos los recursos deExoPlayerpara 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":

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.