[Windows Phone 8.1] Reproducir Audio en Background

Introducción

Una tarea habitual a realizar en aplicaciones es la reproducción de
archivos de audio ya sean podcasts, música, sonidos, etc. En muchas de
las situaciones la reproducción debe continuar cuando la aplicación pasa
a background.

En este artículo vamos a crear un reproductor de audio con continuidad al pasar a background.

¿Te apuntas?

Un poco de teoría antes de comenzar…

En Windows Phone 8.0 ya podíamos realizar esta acción, reproducir
audio en background. Con la llegada de las aplicaciones Universales con
Windows Phone 8.1, la forma de crear la tarea en background es
diferente, algo más similar a la forma ya disponible en WinRT aunque
tampoco igual. Esto nos permite crear la tarea en background de audio
compartiendo  código aunque no sería exactamente el mismo, hay
diferencias entre la implementación del agente en background para
Windows Phone y Windows Store. Además tendremos acceso a nuevas
características previamente no disponibles como trabajar con la
velocidad de reproducción por ejemplo.

NOTA: Al actualizar una Aplicación Windows Phone 8.0 a Silverlight 8.1 que implementase una tarea en background de audio hay que tener en cuenta que el AudioPlayerAgent no esta soportado.

Queremos reproducir audio cuando nuestra interfaz de usuario no este
en primer plano. Para ello, utilizaremos una tarea en background capaz
de reproducir audio.En el espacio de nombres Windows.Media.Playback
contamos con un conjunto de APIs destinadas a ofrecernos la posibilidad
de reproducir audio en segundo plano (incluso en primer plano en caso
necesario). Usando esta API utilizaremos un MediaPlayer global encargado de llevar a cabo la reproducción.

La reprodución del audio se realizará desde background mientras que la App accederá a la información del MediaPlayer
vía objeto proxy. Concretamente la comunicación se realizará por un
sencillo sistema de mensajería. Se pueden enviar mensajes desde primer plano a segundo plano y viceversa.

NOTA: Un mensaje puede ser desde una simple cadena a un conjunto de valores.

Veamos el diagrama de como sería el sistema:

Cuando queremos reproducir audio en una tarea de fondo en Windows Phone estamos tratando con dos procesos.
Por un lado contamos con un proceso en primer plano, nuestra App con
nuestra interfaz de usuario y por otro lado una tarea en segundo plano
que contará con la lógica para reproducir el audio. Esto es asi ya que
si el sistema o el usuario suspende o finaliza el primer proceso, el
audio seguiría reproduciendose desde el segundo.

Nuestra UI

Comenzamos creando un nuevo proyecto:

Añadimos las carpetas Views, ViewModels y Services además de las clases base necesarias para implementar el patrón MVVM de la misma forma que vimos en este artículo.

En este ejemplo tendremos eventos del //BUILD 2014 de Channel 9. Nuestro objetivo será crear una App capaz de reproducir el audio de los eventos funcionando por supuesto en segundo plano.

Comenzamos creand el modelo:

public class Event
{
     public string Name { get; set; }
 
     public string Image { get; set; }
 
     public string Duration { get; set; }
 
     public string Url { get; set; }
}

Una sencilla clase que nos permita almacenar toda la información relacionada con un evento. Los valores principales será la Url donde tendremos el acceso al audio y la propiedad Name que nos indicará que se esta reproduciendo.

En nuestra viewmodel, cargaremos la información de un evento:

private Event LoadEvent()
{
     return new Event
     {
          Name = "What’s New for Windows and Windows Phone Developers",
          Image = "ms-appx:///Assets/Build.jpg",
          Duration = "27 minutes, 43 seconds",
     };
}

Lo llamaremos cuando la vista pase a ser la activa, es decir, al entrar en la vista, sobreescritura del método OnNavigatedTo:

public override Task OnNavigatedTo(NavigationEventArgs args)
{
     // Cargamos los datos del evento
     Event = LoadEvent();
 
     return null;
}

Una vez cargada la información del evento contaremos en nuestra interfaz con un botón para controlar la reproducción (PlayStop). Definimos una pequeña enumeración para que la gestión del estado sea sencilla:

public enum PlayerState
{
     Play,
     Pause
};
 
private PlayerState _state;
public PlayerState State
{
     get { return _state; }
     set
     {
          _state = value;
          RaisePropertyChanged();
     }
}

También necesitaremos el comando a ejecutar en la viewmodel al pulsar sobre el botón:

private ICommand _playerCommand;
 
public ICommand PlayerCommand
{
     get { return _playerCommand = _playerCommand ?? new DelegateCommand(PlayerCommandExecute); }
}
 
private void PlayerCommandExecute()
{
     if(State == PlayerState.Play)
          // Play
     else
          // Stop
}

De modo que la definición del botón en nuestra interfaz sería:

<Button VerticalAlignment="Stretch"
        BorderBrush="{x:Null}" Width="50"
        Command="{Binding PlayerCommand}">
     <Image Source="{Binding State, Converter={StaticResource StateToIconConverter}}"/>
</Button>

Donde la imagen la gestiona un Converter. El Converter devuelve un icono de Play o Stop segun el estado:

public class StateToIconConverter : IValueConverter
{
     private const string Play = "ms-appx:///Assets/Play.png";
     private const string Stop = "ms-appx:///Assets/Stop.png";
 
     public object Convert(object value, Type targetType, object parameter, string language)
     {
          var state = value as PlayerViewModel.PlayerState?;
 
          if (state == null)
              return string.Empty;
 
          return state == PlayerViewModel.PlayerState.Play ? Play : Stop;
     }
 
     public object ConvertBack(object value, Type targetType, object parameter, string language)
     {
          return null;
     }
}

El resultado de nuestra UI es el siguiente:

NOTA: Para simplificar el ejemplo se han
suprimido ciertas partes de código no necesarias para el objetivo
principal del artículo, el audio en background. Hablamos de código como
el estilo del botón de reproducción o el XAML general de la vista. En la
parte inferior del artículo esta disponible todo el código fuente del
ejemplo.

Creando la tarea en background

Teniendo una aplicación Windows Phone 8.1 nos centramos en añadir la background task. Para añadir la background task debemos añadir un componente WinRT.

Una vez creado el componente WinRT renombraremos la clase a UpdateTask. La clase BackgoundAudioTask implementa la interfaz IBackgroundTask. Esta interfaz cuenta con un único método llamado Run.

public sealed class BackgroundAudioTask : IBackgroundTask
{
    public void Run(IBackgroundTaskInstance taskInstance)
    {
              
    }
}

NOTA:  La clase de la tarea en segundo plano debe ser una clase public y sealed.

Comenzamos a escribir el código necesario en la tarea en background para realizar la reproducción de audio:

private BackgroundTaskDeferral _deferral;
private SystemMediaTransportControls _systemMediaTransportControl;
private MediaPlayer _mediaPlayer;

Creamos las variables globales necesarias. Antes de continuar vamos a
determinar su cometido. Comenzamos hablando de la variable de tipo BackgroundTaskDeferral. La tarea en segundo plano es iniciada por el proceso en primer plano haciendo una llamada aBackgroundMediaPlayer.Current. Tras esa llamada se lanza el método IBackgroundTask.Run donde se inicia la variable _deferral con el objetivo de completar el aplazamiento, la reproducción en los eventos Canceled o Completed.

SystemMediaTransportControls representa los controles multimedia del sistema.

Los utilizaremos para gestionar el audio cuando nuestra Aplicación no
se encuentre en primer plano (Ejemplo: Pantalla de bloqueo).

Por último, la variable de tipo MediaPlayer será la que nos exponga
los métodos necesarios para comenzar y detener la reproducción del
audio.

Continuamos. Vamos a definir el código del método Run:

public void Run(IBackgroundTaskInstance taskInstance)
{
     // La clase SystemMediaTransportControls permite a tu aplicación usar los controles de
     // transporte multimedia del sistema proporcionados por Windows y actualizar la información
     // multimedia que se muestra.
     _systemMediaTransportControl = SystemMediaTransportControls.GetForCurrentView();
     _systemMediaTransportControl.IsEnabled = true;
 
     BackgroundMediaPlayer.MessageReceivedFromForeground += MessageReceivedFromForeground;
     BackgroundMediaPlayer.Current.CurrentStateChanged += BackgroundMediaPlayerCurrentStateChanged;
 
     taskInstance.Canceled += OnCanceled;
     taskInstance.Task.Completed += Taskcompleted;
 
     _deferral = taskInstance.GetDeferral();
}

Aparte de instanciar las variables globales vistas previamente cabe destacar la suscripción a dos eventos fundamentales:

  • MessageReceivedFromForeground: Este evento se lanzará cada vez que un mensaje desde la UI sea enviado.
  • CurrentStateChanged: Este evento se lanzará cada vez que el estado del MediaPlayer cambie entre Playing, Paused o Stopped.

Vemos la definición de los eventos recibidos desde la aplicación:

private void MessageReceivedFromForeground(object sender, MediaPlayerDataReceivedEventArgs e)
{
     ValueSet valueSet = e.Data;
     foreach (string key in valueSet.Keys)
     {
          switch (key)
          {
               case "Play":
                    Play(valueSet[key].ToString(), valueSet["Title"].ToString());
                    break;
               case "Pause":
                    Pause();
                    break;
          }
     }
}

Sencillo. Recordad que la UI se comunica con la tarea en segundo
plano vía mensajes. En este evento capturamos los mensajes y los
interpretamos. Podemos recibir dos tipos de mensajes desde la UI,
comenzar la reproducción detenerla. Asi que en función de la Key recibida lanzamos un método Play o un método Pause.

Veamos por lo tanto la definición de los métodos base, Play y Pause:

private void Play(string url, string title)
{
     _mediaPlayer = BackgroundMediaPlayer.Current;
     _mediaPlayer.AutoPlay = true;
     _mediaPlayer.SetUriSource(new Uri(url));
 
     _systemMediaTransportControl.ButtonPressed += MediaTransportControlButtonPressed;
     _systemMediaTransportControl.IsPauseEnabled = true;
     _systemMediaTransportControl.IsPlayEnabled = true;
     _systemMediaTransportControl.DisplayUpdater.Type = MediaPlaybackType.Music;
     _systemMediaTransportControl.DisplayUpdater.MusicProperties.Title = title;
     _systemMediaTransportControl.DisplayUpdater.Update();
}

El método Play define la fuente del audio en el objeto MediaPlayer y actualiza toda la información del reproductor SystemMediaTransportControl.

El método Pause:

Lanza el método Pause del MediaPlayer.

private void Pause()
{
     if (_mediaPlayer == null)
          _mediaPlayer = BackgroundMediaPlayer.Current;
 
     _mediaPlayer.Pause();
}

Debemos controlar que el PlaybackStatus del control SystemMediaTransportControl se ve reflejado en el estado de nuestro MediaPlayer:

private void BackgroundMediaPlayerCurrentStateChanged(MediaPlayer sender, object args)
{
     switch (sender.CurrentState)
     {
          case MediaPlayerState.Playing:
               _systemMediaTransportControl.PlaybackStatus = MediaPlaybackStatus.Playing;
               break;
          case MediaPlayerState.Paused:
               _systemMediaTransportControl.PlaybackStatus = MediaPlaybackStatus.Paused;
               break;
     }
}

También debemos gestionar la pulsación de botones en el control SystemMediaTransportControl:

private void MediaTransportControlButtonPressed(SystemMediaTransportControls sender,
            SystemMediaTransportControlsButtonPressedEventArgs args)
{
     switch (args.Button)
     {
          case SystemMediaTransportControlsButton.Play:
               BackgroundMediaPlayer.Current.Play();
               break;
          case SystemMediaTransportControlsButton.Pause:
               BackgroundMediaPlayer.Current.Pause();
               break;
     }
}

Y por supuesto, debemos cerrar correctamente la tarea de fondo. En caso de finalización o cancelación detenemos la reproducción:

private void Taskcompleted(BackgroundTaskRegistration sender, BackgroundTaskCompletedEventArgs args)
{
     BackgroundMediaPlayer.Shutdown();
     _deferral.Complete();
}
 
private void OnCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason)
{
     BackgroundMediaPlayer.Shutdown();
     _deferral.Complete();
}

Cerramos aqui el código de nuestra tarea en background. Continuamos
viendo como vincular nuestro proyecto Windows Phone 8.1, la UI, con la
tarea.

En nuestro proyecto Windows Phone 8.1 hacemos clic derecho, opción “Add references”:

Tras añadir la referencia al componente WinRT debemos realizar algunos cambios en el archivo Package.appxmanifiest. Nos dirigimos a la pestaña “Capabilites”. Añadimos una nueva capacidad de tipo Background Task:

En las propiedades debemos definir el tipo a Audio la propiedad Entry Point, es decir, el nombre completo de la clase de nuestra background task incluido namespace:

Y todo listo!

Integrándolo todo

Con la tarea en background definida y referenciada en nuestro proyecto Windows Phone es hora de integrarlo todo. Vamos a utilizar la tarea en segundo plano en el comando que gestiona la reproducción en la viewmodel, lo recordamos:

private void PlayerCommandExecute()
{
     if(State == PlayerState.Play)
          // Play
     else
          // Stop
}

En los comentarios realizaremos llamadas a metodos Play() Y Stop() respectivamente. Definimos el método de reproducción:

private void Play()
{
     State = PlayerState.Pause;
     BackgroundMediaPlayer.SendMessageToBackground(new ValueSet
     {
          {
               "Play",
               _event.Url
          },
          {
               "Title",
               _event.Name
          }
     });
}

Enviamos un mensaje a nuestra tarea en segundo
plano. En la Key le indicamos la acción a ejecutar, la reproducción,
pasando en el valor la Url con el audio a reproducir. Podemos pasar
tantos parámetros como necesitemos. No estamos limitados a una sencilla
cadena. En este ejemplo también se pasa el nombre del audio a reproducir
aunque también podría ser interesante la portada del evento/album, el
autor o artista, etc.

Ahora pasamos al método de detención de la reproducción.

private void Pause()
{
     State = PlayerState.Play;
     BackgroundMediaPlayer.SendMessageToBackground(new ValueSet
     {
          {
               "Pause",
               string.Empty
          }
     });
}

De esta forma, nuestro comando quedara de la siguiente forma:

private void PlayerCommandExecute()
{
     if(State == PlayerState.Play)
          Play();
     else
          Pause();
}

Y hasta aqui ya lo tenemos todo listo!. El resultado final es el siguiente:

Podéis descargar el ejemplo realizado a continuación:

Y hasta aquí llega el artículo de hoy. Como siempre espero que os
resulte interesante. Recordar que cualquier tipo de duda o sugerencia la
podéis dejar reflejada en los comentarios.

En próximos artículos veremos como realizar la misma operación en una
Aplicaicón Windows Store para Windows 8.1 entre otras novedades.

Más información

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *