Débutant

Ecouter de la musique avec Windows 10

Etant un grand fan de musique, je passe mon temps avec des écouteurs dans les oreilles. Il y a quelques temps, j’ai voulu voir ce qu’il y avait sous le capot et regarder un peu le moteur. J’ai décidé de réaliser une application d’écoute de musique.

Si on utilise directement dans le XAML un MediaPlayer, le problème de la lecture en Background se présentera. Pour éviter cela, il n’y qu’une seule chose à faire, c’est de mettre la logique de lecture dans une BackgroundTask et utiliser un BackgroundMediaPlayer.

Pour cet article, je me suis inspiré et ai utilisé du code provenant du Github de Microsoft.

L’interface graphique

Interface graphique du player musical Windows 10

L’interface est simple. Elle comprend des boutons qui permettent d’interagir avec le player et une liste qui permet de choisir la chanson.

Cette interface, et donc cette application, n’ont que pour objectif de montrer les concepts de BackgroundMediaPlayer et de lecture de fichier audio.

Le PlayListManager

Pour mes besoins, j’ai défini un playlist item.

public string Artist { get; set; }
public string Album { get; set; }
public string Title { get; set; }
public string Uri { get; set; }
public string ImageUri { get; set; }

Ensuite j’ai créé une classe PlayListManager qui est une classe qui implémente une IList.
Cette classe a connaissance du BackgroundMediaPlayer et écoute les événements suivants du BackGroundMediaPlayer :

  • MediaOpened
  • MediaEnded
  • CurrentStateChanged
  • MediaFailed

Elle possède aussi un event pour notifier du changement de chanson : TrackChanged.

Lors de la demande de changement de chanson, l’utilisateur fera appel à cette méthode :


public void StartTrackAt(int id) { if (id != CurrentTrackId) { PlaylistItem source = _listPlayListItem[id]; CurrentTrackId = id; _mediaPlayer.AutoPlay = false; _mediaPlayer.SetUriSource(new Uri(source.Uri)); } }

Ici on remarque que l’on met une valeur dans la source du media player, ce qui aura plusieurs incidences : des appels à CurrentStateChanged seront effectués, ainsi qu’un appel à MediaOpened.

Lors de l’appel à MediaOpened, la lecture du média et de l’event TrackChanged seront lancés.
Enfin lorsque le média est fini, MediaEnded est appelé, ceci nous permettra de passer au média suivant.

Creation d’une BackgroundTask Audio

Dans un premier temps, il faut ajouter un projet de type Composant Windows Runtime, et ajouter une classe qui se nommera AudioTask et implémentera IBackgroundTask.
Il faut ensuite définir dans le Package.appxmanifest la background task qui sera de type audio.

Le point d’entrée de notre classe sera donc la méthode :


public void Run(IBackgroundTaskInstance taskInstance)

Pour le fonctionnement de la classe, nous avons besoin d’un certain nombre d’objets, parmi lesquels :


private BackgroundTaskDeferral _deferral; private AutoResetEvent _backgroundTaskStarted = new AutoResetEvent(false);

Le backgroundTaskDeferral permet de notifier le système du fait que la Task peut continuer de vivre en “dehors” de la méthode Run.
L’AutoResetEvent nous permettra d’attendre que la tâche soit correctement initialisée avant de continuer le process de lecture de fichier audio.

La méthode Run

Gestion de vie de la BackgroundTask

La première chose à faire est de brancher les événements pour la gestion de la vie de la BackgroundTask.


taskInstance.Canceled += new BackgroundTaskCanceledEventHandler(OnCanceled); taskInstance.Task.Completed += Taskcompleted; private void OnCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason) { BackgroundMediaPlayer.Shutdown(); } void Taskcompleted(BackgroundTaskRegistration sender, BackgroundTaskCompletedEventArgs args) { _deferral.Complete(); }

Pour le moment nos méthodes sont vides, mais au fur et à mesure nous y ajouterons du code.

Obtention de l’état du Foreground et du Background

Malheureusement, il n’existe pas beaucoup de moyens, pour le Foreground, de connaître l’état de fonctionnement de la BackgroundTask et vice versa.
Le seul moyen est d’utiliser un fichier dans lequel on va écrire dedans l’état de nos différents éléments. Je reviendrai plus tard sur la classe qui permet l’écriture dans un fichier, néanmoins, celle-ci n’a rien de complexe.
Cependant, il y a des nouveaux objets à créer :

  • une enum avec les états de l’application
  • une classe contenant une série de constantes utilisées lors de la communication entre Foreground et Background

internal class Constants { public const string CurrentTrack = "trackname"; public const string BackgroundTaskStarted = "BackgroundTaskStarted"; public const string BackgroundTaskRunning = "BackgroundTaskRunning"; public const string BackgroundTaskCancelled = "BackgroundTaskCancelled"; public const string AppSuspended = "appsuspend"; public const string AppResumed = "appresumed"; public const string StartPlayback = "startplayback"; public const string SkipNext = "skipnext"; public const string Position = "position"; public const string AppState = "appstate"; public const string BackgroundTaskState = "backgroundtaskstate"; public const string SkipPrevious = "skipprevious"; public const string Trackchanged = "songchanged"; public const string ForegroundAppActive = "Active"; public const string ForegroundAppSuspended = "Suspended"; public const string AddSongToQueue = "addtoqueue"; } enum ForegroundAppStatus { Active, Suspended, Unknown }

Je dois avouer ici, que je n’ai rien inventé et que cela provient du sample mis à disposition par Microsoft sur son GitHub.
Du coup, lors du l’appel à la méthode Run, on va vérifier l’état de fonctionnement du Foreground. S’il fonctionne, on lui enverra une notification pour lui dire que la tâche démarre :


var value = ApplicationSettingsHelper.ReadResetSettingsValue(Constants.AppState); if (value == null) { _foregroundStatus = ForegroundAppStatus.Unknown; } else { _foregroundStatus = (ForegroundAppStatus)Enum.Parse(typeof(ForegroundAppStatus), value.ToString()); } if (_foregroundStatus != ForegroundAppStatus.Suspended) { ValueSet message = new ValueSet(); message.Add(Constants.BackgroundTaskStarted, ""); BackgroundMediaPlayer.SendMessageToForeground(message); }

Et enfin, on notifiera le fait que la tâche est démarrée et on sauvegardera son état :


_backgroundTaskStarted.Set(); _backgroundTaskRunning = true; ApplicationSettingsHelper.SaveSettingsValue(Constants.BackgroundTaskState, Constants.BackgroundTaskRunning); _deferral = taskInstance.GetDeferral();

Création du BackgroundMediaPlayer

Le BackgroundMediaPlayer est LE contrôle qui sera utilisé pour écouter de la musique lorsque l’application n’est pas au premier plan. De fait, c’est même toujours lui qui joue la musique, que l’application soit au premier plan ou non.
Pour ce faire, nous devons récupérer une instance de cet objet, définir ses capacités et s’abonner à quelques événements.

//Récupération de l'instance du player
_systemControls = BackgroundMediaPlayer.Current.SystemMediaTransportControls;

//Evenement du backgroundmediaplayer
BackgroundMediaPlayer.Current.CurrentStateChanged += Current_CurrentStateChanged;
BackgroundMediaPlayer.MessageReceivedFromForeground += BackgroundMediaPlayer_MessageReceivedFromForeground;
_systemControls.ButtonPressed += _systemControls_ButtonPressed;
_systemControls.PropertyChanged += _systemControls_PropertyChanged;

//propriétés définissant le comportement du player
_systemControls.IsEnabled = true;
_systemControls.IsPauseEnabled = true;
_systemControls.IsPlayEnabled = true;
_systemControls.IsNextEnabled = true;
_systemControls.IsPreviousEnabled = true;

Comme pour la gestion de la Task, je parlerai plus tard des abonnements.

Gestion de la vie de la BackgroundTask

Nous allons ici définir le comportement de la BackgroundTask lors de son annulation et lors de sa fin “normale”.


void Taskcompleted(BackgroundTaskRegistration sender, BackgroundTaskCompletedEventArgs args) { Debug.WriteLine("MyBackgroundAudioTask " + sender.TaskId + " Completed..."); ApplicationSettingsHelper.SaveSettingsValue(Constants.AppState, Enum.GetName(typeof(ForegroundAppStatus), _foregroundStatus)); _deferral.Complete(); } private void OnCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason) { // You get some time here to save your state before process and resources are reclaimed Debug.WriteLine("MyBackgroundAudioTask " + sender.Task.TaskId + " Cancel Requested..."); try { //save state ApplicationSettingsHelper.SaveSettingsValue(Constants.CurrentTrack, _playlistManager.GetCurrentItem); ApplicationSettingsHelper.SaveSettingsValue(Constants.Position, BackgroundMediaPlayer.Current.Position.ToString()); ApplicationSettingsHelper.SaveSettingsValue(Constants.BackgroundTaskState, Constants.BackgroundTaskCancelled); ApplicationSettingsHelper.SaveSettingsValue(Constants.AppState, Enum.GetName(typeof(ForegroundAppStatus), _foregroundStatus)); _backgroundTaskRunning = false; //unsubscribe event handlers _systemControls.ButtonPressed -= _systemControls_ButtonPressed; _systemControls.PropertyChanged -= _systemControls_PropertyChanged; _playlistManager.TrackChanged -= _playlistManager_TrackChanged; //clear objects task cancellation can happen uninterrupted _playlistManager.Clear(); _playlistManager = null; BackgroundMediaPlayer.Shutdown(); // shutdown media pipeline } catch (Exception ex) { Debug.WriteLine(ex.ToString()); } _deferral.Complete(); // signals task completion. Debug.WriteLine("MyBackgroundAudioTask Cancel complete..."); }

Dans le cas de sa fin “normale”, nous sauvegarderons l’état de l’application.
Par contre, dans le cas d’une fin “imprévue”, nous sauvegarderons plus de choses, comme l’état d’avancement de lecture ou la chanson en train d’être jouée. On peut faire un peu tout ce que l’on veut en fait.

BackgroundMediaPlayer

Le BackgroundMediaPlayer a besoin de recevoir des informations depuis le Foreground. La réception des messages se fera via le BackgroundMediaPlayer_MessageReceivedFromForeground.

L’envoi depuis le Foreground se fait de la sorte :

var message = new ValueSet();
message.Add(Constants.StartPlayback, Convert.ToInt32(e.NewValue).ToString());
BackgroundMediaPlayer.SendMessageToBackground(message);

La réception, comme nous l’avons vue, se fait via l’event BackgroundMediaPlayer_MessageReceivedFromForeground et est traitée de la sorte :

void BackgroundMediaPlayer_MessageReceivedFromForeground(object sender, MediaPlayerDataReceivedEventArgs e)
        {
            foreach (var key in e.Data.Keys)
            {
                string tmpKey = key.ToString();
                switch (tmpKey.ToLower())
                {
                    case Constants.AddSongToQueue:
                        ....
                }
            }
        }

Les messages échangés sont de type ValueSet, c’est à dire un dictionnaire. Les clefs des entrées seront définies par les valeurs présentes dans la classe Constants (voir le paragraphe Obtention de l’état du Foreground et du Background).
Du coup, lors de la réception du message dans la BackgroundTask, on va pouvoir identifier le traitement à faire.

Attention, les informations passées par ValueSet doivent être sérialisables.

Le BackgroundMediaPlayer peut aussi avoir besoin d’envoyer des messages au Foreground, par exemple lors du changement d’une chanson. Cela se fera en utilisant la méthode static du BackgroundMediaPlayer, SendMessageToForeground.

Cela se fera de cette manière :

ValueSet message = new ValueSet();
message.Add(Constants.Trackchanged, sender.GetCurrentItem.ToString());
BackgroundMediaPlayer.SendMessageToForeground(message);

La réception coté Foreground se fait via l’écoute de l’event MessageReceivedFromBackground.

async void BackgroundMediaPlayer_MessageReceivedFromBackground(object sender, MediaPlayerDataReceivedEventArgs e)
        {
            foreach (string key in e.Data.Keys)
            {
                switch (key)
                {
                    case Constants.Trackchanged:
                        ....
                }
            }
        }

Toutes les demandes de changement de chanson provenant de la partie Foreground passeront par la BackgroundTask. Ainsi, nous aurons le Foreground qui enverra au Background, via la méthode SendMessageToBackground, des informations sur le nouveau média à jouer, cette requête sera transmisse au PlayListManager qui s’occupera de changer la chanson jouée.
De même, lorsque le PlayListManager change de chanson, ou lorsqu’une chanson se termine, un event est lancé, la BackgroundTask l’intercepte et notifie le Foreground via SendMessageToForeground.

Schéma explicatif pour les communications entre Foreground et Background

Fonctionnement sur PC et téléphone

L’application fonctionnera de manière similaire sur les deux plateformes, à la différence près que, sur PC, lorsque l’utilisateur ferme l’application, la BackgroundTask est “tuée”, alors que sur téléphone, elle continuera sa vie jusqu’à ce qu’un autre process ayant besoin de la ressource audio se lance.

Conclusion

Comme nous venons de le voir, la mise en place d’un lecteur musical prenant en charge la notion de background n’est pas triviale et se repose sur le trio Foreground, Background et PlayListManager.

Nombre de vue : 204

COMMENTAIRES 1 commentaire

  1. Aymeric A dit :

    Comment ça, David n’est pas le seul fan de musique de Soat !

    Félicitations pour cet article, Marc

AJOUTER UN COMMENTAIRE