Hola vengo a presentaros un pequeño tutorial para que aprendáis a hacer un Reproductor de listas de canciones para Android al cual le paséis una lista de canciones independiente del formato y la reproduzca hasta terminarla, también observaréis que el reproductor se ejecutará sobre una hebra, por lo que no interrumpirá vuestras tareas y podréis escuchar la música mientras hacéis otras cosas.
Empecemos por las variables que vamos a necesitar:
/** * Created by Emilio Chica Jiménez on 03/08/2016. */ public class Reproductor { private MediaPlayer mp; //Listado de pistas de audio private String[] tracks; private int currentTrack = 0; private Context context; //Guarda el milisegundo por el que se ha pausado una pista private int pauseMoment; private boolean pause=false; //Si se avanza con el seekbar guarda la posicion por donde esta private int currentPosition=0; //Listado de duraciones de las pistas private int[] durations; //Variable para seguir el patrón Singleton private static Reproductor reproductor=null; //Manejador para actualizar la seekbar private Handler mHandler = new Handler(); private SeekBar progressBar =null; //El total de la duracion de las pistas private int durationTracks=0; //Sirve para comprobar si te están llamando y pausar la reproducción private TelephonyManager mgr;
Ya sabemos que necesitamos pasemos a los métodos que tiene que contener:
Primero debéis de saber qué es el patrón Singleton pues lo vamos a usar para programar nuestro reproductor pues ayuda a que sea eficiente y no tener más de una instancia de un objeto reproductor repartida por ahí para implementar este patrón a parte de la variable:
//Variable para seguir el patrón Singleton
private static Reproductor reproductor=null;
Vamos a necesitar el constructor a juego con dicha variable:
/** * Constructor privado, patron Singleton */ private Reproductor(){ } /** * Instancia del patron Singleton * @return */ public static Reproductor getInstance(){ if(reproductor==null){ reproductor = new Reproductor(); }else { } return reproductor; }
Como resultado vamos a poder crear una única instancia de nuestro Reproductor y utilizarla donde queramos llamando al método getInstance(). Lo siguiente que vamos a necesitar son métodos set y get para el contexto de nuestra app:
public Context getContext() { return context; } /** * Es obligatorio establecer el contexto del Reproductor antes de usar nada * @param context */ public void setContext(Context context) { this.context = context; }
Para trabajar con pistas de audio o canciones ahora necesitamos establecer nuestra variable tracks con el listado de canciones que nos proporcione el usuario:
public String[] getTracks() {
return tracks;
}
/**
* Para el reproductor y establece las pistas y su duracion
* @param tracks
*/
public void setTracks(String[] tracks){
stop();
this.tracks = tracks;
this.durations = new int[tracks.length];
durationTracks = getDurationList();
}
Como vemos hay un método stop que será explicado más adelante y un método getDurationList que nos devolverá la duración total de todas las canciones y que vamos a explicar a continuación:
/** * Devuelve la duración de las pistas en un total * @return */ private int getDurationList(){ int duration=0; MediaMetadataRetriever mmr = new MediaMetadataRetriever(); for(int i=0;i<tracks.length;++i) { Uri mediaPath = Uri.parse(tracks[i]); mmr.setDataSource(context,mediaPath); durations[i] = Integer.parseInt(mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)); duration += durations[i]; } durationTracks=duration; return duration; } /** * Devuelve de la pista por la que se encuentra el milisegundo por el que se encuentra * @return */ public int currentPositionInList(){ int current =0; if(mp!=null) { for (int i = 0; i < currentTrack; ++i) current += durations[i]; currentPosition = current + mp.getCurrentPosition(); } return currentPosition; }
El primer método getDurationList obtiene de la ruta donde se encuentre el archivo la duración del mismo con la clase MediaMetadataRetriever y lo acumula para luego devolver el tiempo.
El segundo método currentPositionInList utiliza la duracion de cada canción y además la posición actual del player para saber donde se encuentra reproduciendo el player.
Hace falta escribir algunos sets y gets que puedan ser de ayuda para el que quiera utilizarlos:
public int getDurationTracks() { return durationTracks; } public int[] getDurations() { return durations; } public void setDurations(int[] durations) { this.durations = durations; } public int getCurrentTrack() { return currentTrack; } public void setCurrentTrack(int currentTrack) { this.currentTrack = currentTrack; }
Entramos en la parte de código que sirve para la reproducción, parada, pausa y reaundación:
1. Primero voy a explicaros el código para reproducir todas las canciones continuamente:
/** * Reproduce toda la lista en una hebra */ public void play(){ if(mp==null) { try { mgr = (TelephonyManager) context.getSystemService(TELEPHONY_SERVICE); if(mgr != null) { mgr.listen(phoneListener, PhoneStateListener.LISTEN_CALL_STATE); } Uri file = Uri.parse(tracks[this.currentTrack]); mp = new MediaPlayer(); mp.setDataSource(context, file); mp.prepare(); new HMplayer(context, mp).execute(); mp.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { currentTrack = (currentTrack + 1); if (currentTrack < tracks.length) { Uri nextTrack = Uri.parse(tracks[currentTrack]); try { mp.reset(); mp.setDataSource(context, nextTrack); mp.prepare(); new HMplayer(context, mp).execute(); pause = false; } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } }else { stop(); } } }); pause = false; updateSeekBar(); } catch (Exception e) { Log.e("Reproducir", "Player failed", e); } } }
Como veis compruebo primero si el objeto MediaPlayer es null esto es porque si no es null significa que nuestro reproductor ya ha sido inicializado y está reproduciendo actualmente por lo que no necesitamos volver a reproducir la canción, en caso de que sea null tenemos que primeramente agregar un escuchador para ver si tienes alguna llamada mientras la reproducción:
mgr = (TelephonyManager) context.getSystemService(TELEPHONY_SERVICE); if(mgr != null) { mgr.listen(phoneListener, PhoneStateListener.LISTEN_CALL_STATE); }
Buscamos el identificador de recursos uniforme (Uri) del archivo actual que queremos reproducir y le decimos a nuestro MediaPlayer que ese va a ser el fichero que tiene que utilizar para comenzar con la reproducción, y que se prepare para ello:
mp = new MediaPlayer(); mp.setDataSource(context, file); mp.prepare();
Parece que ahora llega la parte en la que se quiere reproducir el fichero y para ello utilizamos una hebra que hemos creado para tal fin:
new HMplayer(context, mp).execute();
El código de la clase HMplayer es bastante sencillo y voy a ponerlo directamente:
/** * Created by Emilio Chica Jiménez on 14/01/2016. */ public class HMplayer extends AsyncTask<Void, Integer, Integer> { Context context; MediaPlayer mPlayer; public HMplayer(Context context, MediaPlayer mPlayer){ this.context = context; this.mPlayer = mPlayer; } @Override protected Integer doInBackground(Void... params) { mPlayer.start(); return 0; } @Override protected void onProgressUpdate(Integer... values) { } @Override protected void onPreExecute() { } @Override protected void onPostExecute(Integer result) { } @Override protected void onCancelled() { } }
Como veis únicamente se le dice al Reproductor que se ponga en marcha y así conseguimos que se haga en paralelo. Pasamos ahora a explicar el listener que tenemos a continuación que es el que nos va a permitir que podamos seguir reproduciendo las siguientes canciones a la actual y terminar nuestra lista de pistas:
mp.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { currentTrack = (currentTrack + 1); if (currentTrack < tracks.length) { Uri nextTrack = Uri.parse(tracks[currentTrack]); try { mp.reset(); mp.setDataSource(context, nextTrack); mp.prepare(); new HMplayer(context, mp).execute(); pause = false; } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } }else { stop(); } } });
Aquí lo que se pretende es que cuando haya terminado de reproducir la canción que se esté actualmente reproduciendo pasemos a la siguiente:
currentTrack = (currentTrack + 1);
Y si hemos llegado al final de la lista que se pare el Reproductor:
if (currentTrack < tracks.length) {
}else { stop(); }
Sino lo que hacemos el volver a preparar el MediaPlayer con la siguiente canción escogida y que la reproduzca:
Uri nextTrack = Uri.parse(tracks[currentTrack]); try { mp.reset(); mp.setDataSource(context, nextTrack); mp.prepare(); new HMplayer(context, mp).execute(); pause = false; } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); }
2. El código siguiente es para pausar la reproducción en el caso de que estuviese reproduciendo:
/** * Pausa la reproducción si hay algo reproduciendose */ public void pause(){ if(mp !=null && mp.isPlaying()) { pauseMoment = mp.getCurrentPosition(); mp.pause(); pause=true; mHandler.removeCallbacks(mUpdateTimeTask); } }
Para ello guardo la posición en el momento en que se pausó para posteriormente usarla para reanudar, pauso el MediaPlayer y elimino la tarea de actualizar el seekBar que veremos más adelante.
3. El código para parar la reproducción se podría escribir como sigue:
/** * Para la reproducción y libera el reproductor */ public void stop(){ if(mp !=null) { mp.setOnCompletionListener(null); mp.release(); mp = null; pause=false; currentTrack=0; mHandler.removeCallbacks(mUpdateTimeTask); progressBar.setProgress(0); if(mgr != null) { mgr.listen(phoneListener, PhoneStateListener.LISTEN_NONE); } } }
- Primero elimino el listener para que no siga con la siguiente canción
- Libero el MediaPlayer
- Pongo a null el MediaPlayer
- Elimino la tarea de actualizar el seekBar
- Le quito el progreso al seekBar
- Elimino el escuchador de las llamadas telefónicas.
4. El código para reanudar la reproducción se escribe como sigue:
/** * Reanuda la reproducción por donde se pausó */ public void resume() { if(mp !=null&& !mp.isPlaying()) { mp.seekTo(pauseMoment); new HMplayer(context, mp).execute(); pause=false; updateSeekBar(); } }
Volvemos a la posición donde estaba reproduciendo el MediaPlayer, ejecutamos la reproducción y actualizamos el seekBar.
Ahora vamos a pasar a la parte del código que tiene que ver con el seekBar que en este caso servirá también como progressBar para ver el progreso y avanzar en el audio:
/** * Devuelve la barra de progreso * return * */ public SeekBar getProgressBar() { return progressBar; } /** * Establece la barra de progreso y el escuchador para actualizar su valor * @param progressBar */ public void setProgressBar(SeekBar progressBar) { this.progressBar = progressBar; progressBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { } @Override public void onStartTrackingTouch(SeekBar seekBar) { // remove message Handler from updating progress bar if(mHandler!=null && mUpdateTimeTask!=null) mHandler.removeCallbacks(mUpdateTimeTask); } @Override public void onStopTrackingTouch(SeekBar seekBar) { if(mHandler!=null && mUpdateTimeTask!=null) mHandler.removeCallbacks(mUpdateTimeTask); if(mp!=null) { int totalDuration = mp.getDuration(); int currentPosition = progressToTimer(seekBar.getProgress(), totalDuration); // forward or backward to certain seconds if (mp != null && mp.isPlaying()) mp.seekTo(currentPosition); // update timer progress again updateSeekBar(); } } }); } /** * Cambia el progreso a tiempo en milisegundos * @param progress - * @param totalDuration * returns current duration in milliseconds * */ private int progressToTimer(int progress, int totalDuration) { int currentDuration = 0; totalDuration = (int) (totalDuration / 1000); currentDuration = (int) ((((double)progress) / 100) * totalDuration); // return current duration in milliseconds return currentDuration * 1000; } /** * Tarea que actualiza el valor del progressBar cada 100 milisegundos */ private Runnable mUpdateTimeTask = new Runnable() { public void run() { progressBar.setProgress((int)(((double)reproductor.currentPositionInList()/durationTracks)*100)); mHandler.postDelayed(this, 100); } }; /** * Ejecuta la tarea de actualizar el progressBar a los 100 milisegundos */ public void updateSeekBar(){ if(mHandler!=null && mUpdateTimeTask!=null) mHandler.postDelayed(mUpdateTimeTask, 100); }
Como vemos hay un getter y un setter en el setter además de establecer el seekBar le añadimos el listener para que cuando el usuario interactúe con el seekBar se pare su actualización y cuando deje de interactuar con el seekBar vuelva a actualizar los valores automáticamente.
El método progressToTimer me sirve para convertir el progreso de la barra a milisegundos para así saber en que posición se encuentra conforme a lo que ha avanzado en el seekBar y poder usarlo en el método setter.
Por último tenemos la tarea que actualiza el progreso de la barra, dependiendo de la posición de canción de la que me encuentre en la lista y la duración total de las pistas sacando así el porcentaje. Para llamar a esta tarea usamos el método updateSeekBar.
Las dos últimas partes de este tutorial es aprender cómo puedo saber si la lista asignada al Reproductor es la misma que le estoy asignando y saber si me están llamando cómo parar la música y cuando termine de hablar reanudarla. Para ello tenemos el método compareList y el listener PhoneStateListener.
/** * Comprueba si la lista de pistas pasada actual es diferente a la que tiene * @param list * @return */ public boolean compareList(String[] list){ boolean flag=true; if(tracks!=null) { if (list.length != tracks.length) flag = false; for (int i = 0; i < list.length && flag; ++i) { if (tracks[i].compareTo(list[i]) != 0) flag = false; } }else flag=false; return flag; }
Pasandole un listado de canciones por parámetro devuelve true o false si las listas son iguales o no.
/** * Sirve para controlar si alguien te esta llamando y pausar la reproduccion o volver a reproduccir cuando acabe */ private PhoneStateListener phoneListener = new PhoneStateListener() { @Override public void onCallStateChanged(int state, String incomingNumber) { try { switch (state) { case TelephonyManager.CALL_STATE_RINGING: { Reproductor reproductor = Reproductor.getInstance(); reproductor.pause(); break; } case TelephonyManager.CALL_STATE_OFFHOOK: { break; } case TelephonyManager.CALL_STATE_IDLE: { //PLAY Reproductor reproductor = Reproductor.getInstance(); reproductor.resume(); break; } default: { } } } catch (Exception ex) { } } };
Este listener necesita de establecer los permisos especiales en AndroidManifest:
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
Y ayuda a saber si te están llamando que debes hacer que en este caso pausamos la reproducción y si han colgado la reanudamos.
Conclusión
Esto todo en este primer tutorial, en el siguiente os enseñaré como usar todo este código para crear vuestra aplicación reproductor continuo de listas de canciones.
Programa seguro y no olvideis comentar que os parece.
Imágen de: Freepick
Comenta!