Frédéric Colin
Windows Media Center et WCF : développez votre maison intelligente
Le développement d'applications pour Windows Media Center est facilité avec l'arrivée du SDK 5.3. Même si l'on sent un modèle objet bien lourd derrière, il devient plus facile d'exposer les fonctionnalités de WMC sous la forme de services WCF.
Par Frédéric Colin publié le 23/06/2008 à 08:04, lu 1394 fois, 10 pages
 7 | L'implémentation
Le métier associé aux services est globalement divisé en 4 grands domaines :
  • Gestion du son
  • Gestion du guide
  • Gestion de la mise en veille
  • Gestion des autres touches
  • Gestion de l'IHM
Toute cette gestion est implémentée dans le schéma de classe suivant :
 
Diagramme de classe des services

Diagramme de classe des services

 
La classe qui supporte l'implémentation des contrats de services définis précédemment est « MediaCenterManagement ». La classe « Application » sert à faire le lien entre les informations fournies par l'addin MediaCenter (donc le modèle objet de Windows Media Center) et la couche Services. Les structures « KEYBDINPUT », « INPUT » et la classe « SendInputCommand » servent à gérer les autres commandes liés à la navigation dans les menus. Enfin, la classe « ChannelEqualityComparer » servira dans les requêtages Linq To Object afin de déterminer une manière de comparer les chaînes TV (« Channel »).
La classe « Application » fournit un propriété « MediaCenterEnvironment.AudioMixer » afin de gérer tous les services en relation avec le son (Mute, VolumeUp, VolumeDown, Volume). Par exemple voici l'implémentation du service permettant de monter le volume :

public void VolumeUp()

{

    CurrentApplication.MediaCenterEnvironment.AudioMixer.VolumeUp();

}

Le modèle objet correspondant au guide des programmes est assez curieux dans son ensemble (relations 1-1, redondance, nombreuses informations non remplies, etc.) et j'ai cherché à le représenter de manière fidèle du point de vue des classes mais incomplète du point de vue des propriétés dans le contrat de données.
J'ai construit l'instance du contrat de données par un ensemble de requête Linq To Object.
  1. La première requête permet de récupérer la liste des chaines :

    (from chan in Guide.CurrentEPG.Channels.Distinct(new ChannelEqualityComparer())where chan.PrimaryService != nullorderby chan.Numberselect createChannel(chan, fromDate, toDate)).ToList();

    Vous noterez l'utilisation de la méthode d'extension « Distinct » avec une classe « comparer » permettant de comparer les chaine une à une. La méthode de comparaison employée est basée sur le « CallSign » (nom en clair de la chaîne)

    public bool Equals(ehiProxy.Channel x, ehiProxy.Channel y)

    {

        if (x.PrimaryService == null || y.PrimaryService == null)

            return (x.PrimaryService == y.PrimaryService);

     

        return (x.PrimaryService.CallSign == y.PrimaryService.CallSign);

    }

    La sélection distincte a été rendue obligatoire du fait que le modèle objet de Windows Media Center renvoyait des chaines dupliquées (probablement sur des fréquences différentes à cause des deux émetteurs proches de chez moi).

    La méthode « createChannel » étant spécialisée dans la création des instances de type « Channel « de mon contrat de données

  2. Création de la chaîne

    private DataContracts.Channel createChannel

    (ehiProxy.Channel channel, DateTime fromDate, DateTime toDate)

    {

        return 

            new DataContracts.Channel()

            {

                Id = channel.Id,

                Number = channel.Number,

                IsActive = channel.IsActive,

                PrimaryService = createService(channel.PrimaryService),

                Scheduling = (

                    from s in channel.ShowsAt(fromDate.ToUniversalTime(),

                        toDate.ToUniversalTime())

                    where s.Program != null

                    select createScheduleEntry(s)

                ).ToList()

            };

    }

    Les points intéressants :
    • Utilisation des initialiseurs par défaut
    • Délégation de la création de l'instance du service à une méthode spécialisée
    • La récupération de la grille des programmes (« Scheduling ») via une requête Linq To Object
    • Le requêtage des éléments d'une grille via l'utilisation de la méthode « ShowAt »
    • Délégation de la création de l'instance d'un élément de la grille à une méthode spécialisée

  3. Création du service

    private DataContracts.Service createService(ehiProxy.Service service)

    {

        return new DataContracts.Service()

        {

            Affiliation = service.Affiliation,

            CallSign = service.CallSign,

            Id = service.Id,

            IsDigital = service.IsDigital,

            IsDisabled = service.IsDisabled,

            IsPayPerView = service.IsPayPerView,

            Name = service.Name,

            VirtualChannelNumber = service.VirtualChannelNumber

        };

    }


  4. Création d'un élément de la grille des programmes

    private DataContracts.ScheduleEntry createScheduleEntry(ehiProxy.ScheduleEntry scheduleEntry)

    {

        return new DataContracts.ScheduleEntry()

        {

            Dolby = scheduleEntry.Dolby,

            EndTime = scheduleEntry.EndTime,

            GenericId = scheduleEntry.GenericId,

            HDTV = scheduleEntry.HDTV,

            Id = scheduleEntry.Id,

            Live = scheduleEntry.Live,

            StartTime = scheduleEntry.StartTime,

            SubTitled = scheduleEntry.SubTitled,

            Program = createProgram(scheduleEntry.Program)

        };

    }


  5. Création d'un programme

    private DataContracts.Program createProgram(ehiProxy.Program program)

    {

        return new DataContracts.Program()

        {

            Description = program.Description,

            DirectorStrs = new List<string>(program.DirectorStrs),

            EpisodeId = program.EpisodeId,

            EpisodeTitle = program.EpisodeTitle,

            ActorStrs = new List<string>(program.ActorStrs),

            GuestsStrs = new List<string>(program.GuestsStrs),

            HasMultipleScheduleEntries = program.HasMultipleScheduleEntries,

            HostsStrs = new List<string>(program.HostsStrs),

            Id = program.Id,

            Language = program.Language,

            StoreId = program.StoreId,

            Title = program.Title,

            Year = program.Year

        };

    }

En bref, rien de bien compliqué si ce n'est qu'il faut penser à initialiser correctement le modèle objet avant de pouvoir requêter correctement :
  1. Utilisation de la classe « LineUp » afin d'initialiser la liste des services et de la récupérer

    Microsoft.MediaCenter.TV.Epg.Lineup lu = new Microsoft.MediaCenter.TV.Epg.Lineup();

    _programs = lu.GetServiceIds();

  2. Paramétrage du répertoire de travail et initialisation de la grille des programmes

    DirectoryInfo info = new DirectoryInfo(Environment.GetFolderPath(Environment.SpecialFolder.System));

    if (Directory.Exists(info.Parent.FullName + @"\ehome"))

    {

        Environment.CurrentDirectory = info.Parent.FullName + @"\ehome";

        Microsoft.Ehome.Epg.Guide.Initialize();

     

        Etc.

    }

Sans les deux paramétrages précédents, la grille des programmes par chaîne n'est pas remontée.

Enfin si vous vous demandez où sont stockées les informations du Guide une fois téléchargées depuis le Web, vous pouvez réflecter l'assembly « ehepg.dll », regarder le contenu de la classe « Guide » et notamment la méthode statique « Initialize() » utilisée précédemment. Cette dernière se sert d'une autre classe interne, via l'une de ses propriétés : « epgFileHelper.CurrentEpgFile ».

En poursuivant l'étude minutieuse du code, il devrait être possible de « calculer » ce chemin d'accès. Toutefois pour des raisons de facilité, j'ai fait un peu de « reflection » afin de l'afficher et voici le code correspondant :

System.Reflection.Assembly assemb

    = System.Reflection.Assembly.Load("ehepg");

 

String s = assemb.GetType("Microsoft.Ehome.Epg.Helper.EpgFileHelper", false, true)

    .GetProperty("CurrentEpgFile").GetValue(null, null);

La valeur ainsi récupérée pour ma machine est :

« C:\ProgramData\Microsoft\eHome\EPG\f1b2556dee484c7f8a3ff50ebd0acfc2.sdf »

Il s'agit d'une base « SQLLite » dont les moyens d'accès ont été développés dans l'assembly « ehepg.dll » (espace de nom « Microsoft.Ehome.Epg.Database ») spécifiquement par Microsoft en implémentant les interfaces ADO.NET qui vont bien :
 
SQLLite ADO.NET

SQLLite ADO.NET

 
Le métier en rapport avec cette partie utilise pleinement la classe « Application » :

Application.SetSuspendState(System.Windows.Forms.PowerState.Suspend, true, true);

Le premier paramètre permet de choisir entre le mode suspendu et le mode hibernation. La première valeur booléenne permet de forcer la machine à passer dans l'état suspendu immédiatement et la deuxième valeur pour désactiver la possibilité de réveil événementiel de la machine.
Cette partie a de loin été la plus complexe à élaborer. En effet, le modèle objet de Media Center n'offre pas ces services en standard (déplacement Haut, bas, gauche, droit, ok, menu TV, etc.). Il a donc fallu trouver une solution spécifique pour y arriver.

La solution qui m'avait initialement effleuré l'esprit était d'utiliser la classe « SendKeys » afin de simuler les touches du clavier. Malheureusement, ce fut impossible du fait que c'est l'addin Média Center qui se chargerait d'envoyer les ordres (puisque cet addin est le processus porteur WCF) et non pas directement une fenêtre Windows. Il m'a donc fallu recréer une classe permettant de réaliser cette opération directement en appelant les APIs bas-niveau du système d'exploitation.

La logique fut donc la suivante :
  1. Etape 1 : définir les combinaisons de touche gérées au travers d'un contrat de données afin de faciliter l'utilisation et la description
  2. Récupérer le handle de la fenêtre Windows supportant le processus porteur de Windows Media Center
  3. Eventuellement restaurer la fenêtre à l'écran si elle avait été iconisée dans la barre de tâche. Pour cela, j'utilise l'API :

    [DllImport("user32.dll")]

    private static extern bool ShowWindow(IntPtr hWnd, WindowShowStyle nCmdShow);

  4. Eventuellement passer la fenêtre au premier plan si elle ne l'était pas afin que les ordres clavier lui soient transmis. Pour cela, j'utilise l'API :

    [DllImport("user32.dll")]

    public static extern bool SetForegroundWindow(IntPtr hWnd);

  5. Ensuite, il reste à gérer les combinaisons de touches de types, Shift, Ctrl, Alt, Win. Le tout étant encapsulé dans une classe dédiée à cela appelée « SendInputCommand ». Pour cette dernière, je me suis basé sur une application développée en 2005 (http://sourceforge.net/projects/mcecontroller/) et qui fournissait une partie du service que je recherchais. J'ai donc fait évoluer cette classe afin qu'elle réponde complètement à mon besoin initial.
A partir de là, le métier du service « SendKey » du contrat de service « IMediaCenterManagement » est simplement d'utiliser correctement la classe « SendInputCommand » qui encapsule correctement cette gestion.
Afin d'éviter tout problème d'appel Cross Domain entre la page HTML qui exécute les appels HTTP et le Host hébergeant les services WCF, j'ai embarqué la page HTML ainsi que l'image afférente directement dans le service WCF et l'ai rendu accessible au travers du contrat de service suivant « IWebManagement » décrit précédemment.

Globalement, le métier de ce service est des plus simples. Il s'agit de retourner le contenu d'un fichier stocké côte à côte de Media Center et de le renvoyer dans le flux http en changeant le ContentType. Ce qui nous donne :

public Stream GetFile(String fileName, String encoding)

{

    DirectoryInfo info

        = new DirectoryInfo(Environment.GetFolderPath(Environment.SpecialFolder.System));

 

    WebOperationContext.Current.OutgoingResponse.ContentType = encoding;

 

    if (Directory.Exists(info.Parent.FullName + @"\ehome"))

    {

        return File.OpenRead(String.Format(@"{0}\{1}\{2}", info.Parent.FullName, "ehome", fileName));

    }

 

    return null;

}

Dans le cadre d'une architecture REST, vous noterez que le contexte opérationnel est accessible au travers de la classe « WebOperationContext ». Cette dernière est une classe helper permettant d'accéder aux propriétés contextuelles des appels WCF courant et fournit notamment un accès aux requêtes et réponses web. Ce qui nous permet de changer le ContentType du flux renvoyé au client. Voici un exemple d'appel de ce service :

http://192.168.0.10:8000/MediaCenter/IWebManagement/GetFile?filename=default.htm&encoding=text/html
 
» Démarrer une discussion