Apps mit Sencha Ext JS erstellen

Dieses Dokument soll Ihnen den Einstieg in die Entwicklung von Chrome-Apps mit dem Framework Sencha Ext JS erleichtern. Um dieses Ziel zu erreichen, sehen wir uns eine von Sencha entwickelte Mediaplayer-App an. Der Quellcode und die API-Dokumentation sind auf GitHub verfügbar.

Diese App erkennt die verfügbaren Medienserver eines Nutzers, einschließlich Mediengeräten, die mit dem PC verbunden sind, und Software, die Medien über das Netzwerk verwaltet. Nutzer können Medien durchsuchen, über das Netzwerk wiedergeben oder offline speichern.

Folgende Schritte sind erforderlich, um eine Mediaplayer-App mit Sencha Ext JS zu erstellen:

  • Manifest erstellen, manifest.json
  • Veranstaltungsseite erstellen, background.js
  • Die Logik der Sandbox App.
  • Kommunikation zwischen der Chrome-App und Dateien in einer Sandbox
  • Ermitteln Sie Medienserver.
  • Medien entdecken und abspielen
  • Medien offline speichern.

Manifest erstellen

Für alle Chrome-Apps ist eine Manifestdatei erforderlich, die die Informationen enthält, die Chrome zum Starten von Apps benötigt. Wie im Manifest angegeben, hat die Mediaplayer-App den Wert „offline_enabled“. Media-Assets können lokal gespeichert, unabhängig von der Verbindung aufgerufen und wiedergegeben werden.

Das Feld "sandbox" wird verwendet, um die Hauptlogik der Anwendung in einer Sandbox an einem eindeutigen Ursprung zu speichern. Alle Inhalte, die in einer Sandbox ausgeführt werden, sind von der Content Security Policy für Chrome-Apps ausgenommen, können jedoch nicht direkt auf die Chrome App APIs zugreifen. Das Manifest enthält auch die Berechtigung "Socket". Die Mediaplayer-App stellt mithilfe der Socket API eine Verbindung zu einem Medienserver über das Netzwerk her.

{
    "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"
            ]
        }
    ]
}

Seite „Termin erstellen“

Alle Chrome-Apps erfordern background.js, um sie zu starten. Die Hauptseite des Mediaplayers (index.html) wird in einem Fenster mit den angegebenen Abmessungen geöffnet:

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;
    });

});

Logik der Sandbox-App

Chrome-Apps werden in einer kontrollierten Umgebung ausgeführt, die eine strenge Content Security Policy (CSP) erzwingt. Die Mediaplayer-App benötigt höhere Berechtigungen, um die Ext JS-Komponenten zu rendern. Zur Einhaltung der CSP und zum Ausführen der Anwendungslogik erstellt die Hauptseite der Anwendung (index.html) einen iFrame, der als Sandbox-Umgebung dient:

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

Der iFrame verweist auf sandbox.html, die die für die externe JavaScript-Anwendung erforderlichen Dateien enthält:

<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>

Das app.js-Skript führt den gesamten externen JS-Code aus und rendert die Mediaplayeransichten. Da dieses Skript in einer Sandbox ausgeführt wird, kann es nicht direkt auf die Chrome App APIs zugreifen. Die Kommunikation zwischen app.js-Dateien und Dateien, die nicht in einer Sandbox ausgeführt werden, erfolgt über die HTML5 Post Message API.

Kommunikation zwischen Dateien

Damit die Mediaplayer-App auf APIs für Chrome-Apps zugreifen kann, z. B. um das Netzwerk nach Medienservern abzufragen, sendet app.js Nachrichten an index.js. Im Gegensatz zu app.js, das in einer Sandbox ausgeführt wird, kann index.js direkt auf die Chrome App APIs zugreifen.

index.js erstellt den iFrame:

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

iframeWindow = iframe.contentWindow;

Und wartet auf Nachrichten aus den Dateien, die in der Sandbox ausgeführt werden:

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);

Im folgenden Beispiel sendet app.js eine Nachricht an index.js, in der der Schlüssel „extension-baseurl“ angefordert wird:

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

index.js empfängt die Anfrage, weist das Ergebnis zu und antwortet, indem sie die Basis-URL zurücksendet:

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

Medienserver ermitteln

Das Ermitteln von Medienservern erfordert eine Menge. Auf übergeordneter Ebene wird der Erkennungsworkflow durch eine Nutzeraktion initiiert, um nach verfügbaren Medienservern zu suchen. Der MediaServer-Controller sendet eine Nachricht an index.js. index.js wartet auf diese Nachricht und ruft bei Empfang Upnp.js auf.

Upnp library verwendet die Socket API der Chrome App, um die Mediaplayer-App mit allen erkannten Medienservern zu verbinden und Mediendaten vom Medienserver zu empfangen. Upnp.js verwendet außerdem soapclient.js zum Parsen der Medienserverdaten. Im weiteren Verlauf dieses Abschnitts wird dieser Workflow ausführlicher beschrieben.

Nachricht posten

Wenn ein Nutzer auf die Schaltfläche „Mediaserver“ in der Mitte der Mediaplayer-App klickt, ruft MediaServers.js discoverServers() auf. Diese Funktion prüft zuerst, ob ausstehende Erkennungsanfragen vorhanden sind. Wenn sie auf „true“ gesetzt ist, werden sie abgebrochen, damit die neue Anfrage initiiert werden kann. Als Nächstes sendet der Controller eine Nachricht an index.js mit einem Schlüssel-upnp-Discovery und zwei Callback-Listener:

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');
    }
});

upnpDiscover() aufrufen

index.js wartet auf die Nachricht „upnp-discover“ von app.js und antwortet durch den Aufruf von upnpDiscover(). Wenn ein Medienserver erkannt wird, extrahiert index.js die Medienserverdomain aus den Parametern, speichert den Server lokal, formatiert die Medienserverdaten und überträgt die Daten per Push an den MediaServer-Controller.

Medienserverdaten parsen

Wenn Upnp.js einen neuen Medienserver erkennt, ruft er eine Beschreibung des Geräts ab und sendet eine Soap-Anfrage, um die Medienserverdaten zu durchsuchen und zu parsen. soapclient.js parst die Medienelemente nach Tag-Namen in einem Dokument.

Mit Medienserver verbinden

Upnp.js stellt über die Chrome App Socket API eine Verbindung zu erkannten Medienservern her und empfängt Mediendaten:

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);
        });
    });
});

Medien entdecken und abspielen

Der MediaExplorer-Controller listet alle Mediendateien in einem Medienserverordner auf und ist für die Aktualisierung der Navigationspfadnavigation im Fenster der Mediaplayer-App verantwortlich. Wenn ein Nutzer eine Mediendatei auswählt, sendet der Controller eine Nachricht mit dem Schlüssel „play-media“ an index.js:

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 wartet auf diese Nachricht und antwortet durch Aufrufen von 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);
}

Medien offline speichern

Der Großteil der Arbeit, Medien offline zu speichern, wird von der filer.js-Bibliothek erledigt. Weitere Informationen zu dieser Bibliothek finden Sie unter Einführung in filer.js.

Der Vorgang wird gestartet, wenn ein Nutzer eine oder mehrere Dateien auswählt und die Aktion „Offline laden“ einleitet. Der MediaExplorer-Controller sendet eine Nachricht mit dem Schlüssel „download-media“ an index.js. index.js wartet auf diese Nachricht und ruft die Funktion downloadMedia() auf, um den Downloadvorgang zu starten:

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

Die Dienstmethode DownloadProcess erstellt eine xhr-Anfrage, um Daten vom Medienserver abzurufen, und wartet auf den Abschlussstatus. Dadurch wird der Onload-Callback initiiert, der den empfangenen Inhalt prüft und die Daten lokal mit der Funktion filer.js speichert:

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);
    }
);

Wenn der Download abgeschlossen ist, aktualisiert MediaExplorer die Liste der Mediendateien und die Baumansicht des Mediaplayers.