Créer des applications avec Sencha Ext JS

L'objectif de ce document est de vous aider à créer des applications Chrome avec le framework Sencha Ext JS. Pour atteindre cet objectif, nous allons découvrir en détail une application de lecteur multimédia développée par Sencha. Le code source et la documentation de l'API sont disponibles sur GitHub.

Cette application détecte les serveurs multimédias disponibles d'un utilisateur, y compris les appareils multimédias connectés au PC et les logiciels qui gèrent les contenus multimédias sur le réseau. Les utilisateurs peuvent parcourir des contenus multimédias, les lire sur le réseau ou les enregistrer hors connexion.

Voici les principales étapes à suivre pour créer une application de lecteur multimédia à l'aide de Sencha Ext JS:

  • Créer le fichier manifeste, manifest.json.
  • Créez une page d'événement, background.js.
  • Logique de l'application bac à sable.
  • Communication entre l'application Chrome et les fichiers en bac à sable
  • Découvrir les serveurs multimédias
  • Explorer et lire des contenus multimédias
  • Enregistrer le contenu multimédia hors connexion.

Créer un fichier manifeste

Toutes les applications Chrome nécessitent un fichier manifeste contenant les informations dont Chrome a besoin pour les lancer. Comme indiqué dans le fichier manifeste, l'application de lecteur multimédia est "offline_enabled". Les éléments multimédias peuvent être enregistrés localement, consultés et lus, quelle que soit la connectivité.

Le champ "sandbox" permet de mettre en bac à sable la logique principale de l'application dans une origine unique. Tout contenu en bac à sable est exempté de la Content Security Policy des applications Chrome, mais ne peut pas accéder directement aux API des applications Chrome. Le fichier manifeste inclut également l'autorisation "socket". L'application de lecteur multimédia utilise l'API de socket pour se connecter à un serveur multimédia sur le réseau.

{
    "name": "Video Player",
    "description": "Features network media discovery and playlist management",
    "version": "1.0.0",
    "manifest_version": 2,
    "offline_enabled": true,
    "app": {
        "background": {
            "scripts": [
                "background.js"
            ]
        }
    },
    ...

    "sandbox": {
        "pages": ["sandbox.html"]
    },
    "permissions": [
        "experimental",
        "http://*/*",
        "unlimitedStorage",
        {
            "socket": [
                "tcp-connect",
                "udp-send-to",
                "udp-bind"
            ]
        }
    ]
}

Créer une page d'événement

Toutes les applications Chrome ont besoin de background.js pour lancer l'application. La page principale du lecteur multimédia, index.html, s'ouvre dans une fenêtre avec les dimensions spécifiées:

chrome.app.runtime.onLaunched.addListener(function(launchData) {
    var opt = {
        width: 1000,
        height: 700
    };

    chrome.app.window.create('index.html', opt, function (win) {
        win.launchData = launchData;
    });

});

Logique de l'application sandbox

Les applications Chrome s'exécutent dans un environnement contrôlé qui applique une Content Security Policy (CSP) stricte. L'application de lecteur multimédia nécessite des droits plus élevés pour afficher les composants Ext JS. Pour respecter CSP et exécuter la logique de l'application, la page principale de l'application, index.html, crée un iFrame qui agit comme un environnement de bac à sable:

<iframe id="sandbox-frame" sandbox="allow-scripts" src="sandbox.html"></iframe>

L'iFrame renvoie vers sandbox.html, qui inclut les fichiers requis pour l'application Ext JS:

<html>
<head>
    <link rel="stylesheet" type="text/css" href="resources/css/app.css" />'
    <script src="sdk/ext-all-dev.js"></script>'
    <script src="lib/ext/data/PostMessage.js"></script>'
    <script src="lib/ChromeProxy.js"></script>'
    <script src="app.js"></script>
</head>
<body></body>
</html>

Le script app.js exécute tout le code Ext JS et affiche les vues du lecteur multimédia. Comme ce script est en bac à sable, il ne peut pas accéder directement aux API Chrome App. La communication entre app.js et les fichiers hors bac à sable s'effectue à l'aide de l'API Post Message HTML5.

Communiquer entre les fichiers

Pour que l'application de lecteur multimédia puisse accéder aux API des applications Chrome, par exemple pour interroger le réseau pour les serveurs multimédias, app.js publie des messages dans le fichier index.js. Contrairement au app.js en bac à sable, index.js peut accéder directement aux API des applications Chrome.

index.js crée l'iFrame:

var iframe = document.getElementById('sandbox-frame');

iframeWindow = iframe.contentWindow;

Il écoute les messages provenant des fichiers en bac à sable:

window.addEventListener('message', function(e) {
    var data= e.data,
        key = data.key;

    console.log('[index.js] Post Message received with key ' + key);

    switch (key) {
        case 'extension-baseurl':
            extensionBaseUrl(data);
            break;

        case 'upnp-discover':
            upnpDiscover(data);
            break;

        case 'upnp-browse':
            upnpBrowse(data);
            break;

        case 'play-media':
            playMedia(data);
            break;

        case 'download-media':
            downloadMedia(data);
            break;

        case 'cancel-download':
            cancelDownload(data);
            break;

        default:
            console.log('[index.js] unidentified key for Post Message: "' + key + '"');
    }
}, false);

Dans l'exemple suivant, app.js envoie un message à index.js demandant la clé "extension-baseurl":

Ext.data.PostMessage.request({
    key: 'extension-baseurl',
    success: function(data) {
        //...
    }
});

index.js reçoit la requête, attribue le résultat et répond en renvoyant l'URL de base:

function extensionBaseUrl(data) {
    data.result = chrome.extension.getURL('/');
    iframeWindow.postMessage(data, '*');
}

Découvrir les serveurs multimédias

La découverte des serveurs multimédias implique beaucoup de travail. De manière générale, le workflow de découverte est lancé par une action de l'utilisateur pour rechercher les serveurs multimédias disponibles. Le contrôleur MediaServer publie un message sur index.js. index.js l'écoute et appelle Upnp.js lorsqu'il le reçoit.

Upnp library utilise l'API de socket de l'application Chrome pour connecter l'application de lecteur multimédia à tous les serveurs multimédias détectés et recevoir les données multimédias de ce serveur. Upnp.js analyse également les données du serveur multimédia à l'aide de soapclient.js. Le reste de cette section décrit ce workflow plus en détail.

Publier le message

Lorsqu'un utilisateur clique sur le bouton "Serveurs multimédias" au centre de l'application de lecteur multimédia, MediaServers.js appelle discoverServers(). Cette fonction vérifie d'abord les requêtes de découverte en attente, puis, si elle est définie sur "true", les abandonne pour que la nouvelle requête puisse être lancée. Ensuite, le contrôleur publie un message sur index.js avec une clé "upnp-discovery" et deux écouteurs de rappel:

me.activeDiscoverRequest = Ext.data.PostMessage.request({
    key: 'upnp-discover',
    success: function(data) {
        var items = [];
        delete me.activeDiscoverRequest;

        if (serversGraph.isDestroyed) {
            return;
        }

        mainBtn.isLoading = false;
        mainBtn.removeCls('pop-in');
        mainBtn.setIconCls('ico-server');
        mainBtn.setText('Media Servers');

        //add servers
        Ext.each(data, function(server) {
            var icon,
                urlBase = server.urlBase;

            if (urlBase) {
                if (urlBase.substr(urlBase.length-1, 1) === '/'){
                        urlBase = urlBase.substr(0, urlBase.length-1);
                }
            }

            if (server.icons && server.icons.length) {
                if (server.icons[1]) {
                    icon = server.icons[1].url;
                }
                else {
                    icon = server.icons[0].url;
                }

                icon = urlBase + icon;
            }

            items.push({
                itemId: server.id,
                text: server.friendlyName,
                icon: icon,
                data: server
            });
        });

        ...
    },
    failure: function() {
        delete me.activeDiscoverRequest;

        if (serversGraph.isDestroyed) {
            return;
        }

        mainBtn.isLoading = false;
        mainBtn.removeCls('pop-in');
        mainBtn.setIconCls('ico-error');
        mainBtn.setText('Error...click to retry');
    }
});

Appeler upnpDiscover()

index.js écoute le message "upnp-discover" de app.js et répond en appelant upnpDiscover(). Lorsqu'un serveur multimédia est détecté, index.js extrait le domaine du serveur multimédia à partir des paramètres, enregistre le serveur localement, met en forme les données du serveur multimédia et les transmet au contrôleur MediaServer.

Analyser les données du serveur multimédia

Lorsque Upnp.js découvre un nouveau serveur multimédia, il récupère une description de l'appareil et envoie une requête Soaprequest pour parcourir et analyser les données du serveur multimédia. soapclient.js analyse les éléments multimédias par nom de balise dans un document.

Se connecter au serveur multimédia

Upnp.js se connecte aux serveurs multimédias découverts et reçoit les données multimédias à l'aide de l'API Chrome App Socket:

socket.create("udp", {}, function(info) {
    var socketId = info.socketId;

    //bind locally
    socket.bind(socketId, "0.0.0.0", 0, function(info) {

        //pack upnp message
        var message = String.toBuffer(UPNP_MESSAGE);

        //broadcast to upnp
        socket.sendTo(socketId, message, UPNP_ADDRESS, UPNP_PORT, function(info) {

            // Wait 1 second
            setTimeout(function() {

                //receive
                socket.recvFrom(socketId, function(info) {

                    //unpack message
                    var data        = String.fromBuffer(info.data),
                        servers     = [],
                        locationReg = /^location:/i;

                    //extract location info
                    if (data) {
                        data = data.split("\r\n");

                        data.forEach(function(value) {
                            if (locationReg.test(value)){
                                servers.push(value.replace(locationReg, "").trim());
                            }
                        });
                    }

                    //success
                    callback(servers);
                });

            }, 1000);
        });
    });
});

Explorer et lire des contenus multimédias

Le contrôleur MediaExplorer répertorie tous les fichiers multimédias d'un dossier de serveur multimédia et est chargé de mettre à jour le fil d'Ariane dans la fenêtre de l'application du lecteur multimédia. Lorsqu'un utilisateur sélectionne un fichier multimédia, le contrôleur publie un message sur index.js avec la touche "play-media" :

onFileDblClick: function(explorer, record) {
    var serverPanel, node,
        type    = record.get('type'),
        url     = record.get('url'),
        name    = record.get('name'),
        serverId= record.get('serverId');

    if (type === 'audio' || type === 'video') {
        Ext.data.PostMessage.request({
            key     : 'play-media',
            params  : {
                url: url,
                name: name,
                type: type
            }
        });
    }
},

index.js écoute ce message et répond en appelant playMedia():

function playMedia(data) {
    var type        = data.params.type,
        url         = data.params.url,
        playerCt    = document.getElementById('player-ct'),
        audioBody   = document.getElementById('audio-body'),
        videoBody   = document.getElementById('video-body'),
        mediaEl     = playerCt.getElementsByTagName(type)[0],
        mediaBody   = type === 'video' ? videoBody : audioBody,
        isLocal     = false;

    //save data
    filePlaying = {
        url : url,
        type: type,
        name: data.params.name
    };

    //hide body els
    audioBody.style.display = 'none';
    videoBody.style.display = 'none';

    var animEnd = function(e) {

        //show body el
        mediaBody.style.display = '';

        //play media
        mediaEl.play();

        //clear listeners
        playerCt.removeEventListener( 'transitionend', animEnd, false );
        animEnd = null;
    };

    //load media
    mediaEl.src = url;
    mediaEl.load();

    //animate in player
    playerCt.addEventListener( 'transitionend', animEnd, false );
    playerCt.style.transform = "translateY(0)";

    //reply postmessage
    data.result = true;
    sendMessage(data);
}

Enregistrer des contenus multimédias hors connexion

Le plus gros travail d'enregistrement des contenus multimédias hors connexion est effectué par la bibliothèque filer.js. Pour en savoir plus, consultez la section Présentation de filer.js.

Le processus démarre lorsqu'un utilisateur sélectionne un ou plusieurs fichiers et lance l'action "Passer hors connexion". Le contrôleur MediaExplorer publie un message sur index.js avec une clé "download-media". index.js écoute ce message et appelle la fonction downloadMedia() pour lancer le processus de téléchargement:

function downloadMedia(data) {
        DownloadProcess.run(data.params.files, function() {
            data.result = true;
            sendMessage(data);
        });
    }

La méthode utilitaire DownloadProcess crée une requête xhr pour obtenir les données du serveur multimédia et attend l'état d'avancement. Cela lance le rappel de chargement, qui vérifie le contenu reçu et enregistre les données localement à l'aide de la fonction filer.js:

filer.write(
    saveUrl,
    {
        data: Util.arrayBufferToBlob(fileArrayBuf),
        type: contentType
    },
    function(fileEntry, fileWriter) {

        console.log('file saved!');

        //increment downloaded
        me.completedFiles++;

        //if reached the end, finalize the process
        if (me.completedFiles === me.totalFiles) {

            sendMessage({
                key             : 'download-progresss',
                totalFiles      : me.totalFiles,
                completedFiles  : me.completedFiles
            });

            me.completedFiles = me.totalFiles = me.percentage = me.downloadedFiles = 0;
            delete me.percentages;

            //reload local
            loadLocalFiles(callback);
        }
    },
    function(e) {
        console.log(e);
    }
);

Une fois le processus de téléchargement terminé, MediaExplorer met à jour la liste des fichiers multimédias et le panneau d'arborescence du lecteur multimédia.