Crea apps con Sencha Ext JS

El objetivo de este documento es que comiences a compilar apps de Chrome con el framework Sencha Ext JS. Para lograr este objetivo, exploraremos en detalle una app de reproductor multimedia creada por Sencha. El código fuente y la documentación de la API están disponibles en GitHub.

Esta app descubre los servidores de medios disponibles del usuario, incluidos los dispositivos multimedia conectados a la PC y el software que administra el contenido multimedia en la red. Los usuarios pueden explorar contenido multimedia, reproducir contenido mediante la red o guardar contenido sin conexión.

Estas son las acciones clave que debes realizar para compilar una app de reproductor multimedia con Sencha Ext JS:

  • Crea el manifiesto, manifest.json.
  • Crea una página del evento, background.js.
  • La lógica de la app de Sandbox
  • Comunicación entre la app de Chrome y los archivos de la zona de pruebas
  • Descubre servidores de medios.
  • Explora y reproduce contenido multimedia.
  • Guardar contenido multimedia sin conexión

Crear manifiesto

Todas las apps de Chrome requieren un archivo de manifiesto que contenga la información que Chrome necesita para iniciar las apps. Como se indica en el manifiesto, la app de reproducción multimedia es "offline_enabled"; los elementos multimedia se pueden guardar de forma local, acceder a ellos y reproducirlos sin importar la conectividad.

El campo "zona de pruebas" se usa para eliminar la lógica principal de la app en un origen único. Todo el contenido de la zona de pruebas está exento de la Política de Seguridad del Contenido de la app de Chrome, pero no puede acceder directamente a las APIs de la app de Chrome. El manifiesto también incluye el permiso "socket"; la app de reproductor multimedia usa la API de socket para conectarse a un servidor multimedia en la red.

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

Página para crear un evento

Todas las apps de Chrome requieren background.js para iniciar la aplicación. La página principal del reproductor multimedia, index.html, se abre en una ventana con las dimensiones especificadas:

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

});

Lógica de la app de zona de pruebas

Las Apps de Chrome se ejecutan en un entorno controlado que aplica una Política de Seguridad del Contenido (CSP) estricta. La app de reproducción multimedia necesita algunos privilegios más altos para procesar los componentes de Ext JS. Para cumplir con la CSP y ejecutar la lógica de la app, index.html, la página principal de la app, crea un iframe que actúa como un entorno de zona de pruebas:

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

El iframe apunta a sandbox.html, que incluye los archivos necesarios para la aplicación 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>

La secuencia de comandos app.js ejecuta todo el código Ext JS y procesa las vistas del reproductor multimedia. Dado que esta secuencia de comandos está en una zona de pruebas, no puede acceder directamente a las APIs de la app de Chrome. La comunicación entre los archivos app.js y los que no están en la zona de pruebas se realiza mediante la API de Post Message de HTML5.

Cómo comunicarse entre archivos

Para que la app de reproductor multimedia acceda a las APIs de la app de Chrome, como consultar la red en busca de servidores multimedia, app.js publica mensajes en index.js. A diferencia de la zona de pruebas app.js, index.js puede acceder directamente a las APIs de la app de Chrome.

index.js crea el iframe:

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

iframeWindow = iframe.contentWindow;

Además, escucha los mensajes de los archivos de la zona de pruebas:

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

En el siguiente ejemplo, app.js envía un mensaje a index.js para solicitar la clave "extension-baseurl":

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

index.js recibe la solicitud, asigna el resultado y responde enviando de vuelta la URL base:

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

Cómo descubrir servidores de contenido multimedia

Hay muchos aspectos para descubrir servidores multimedia. En un nivel superior, el flujo de trabajo de descubrimiento se inicia mediante una acción del usuario para buscar servidores de medios disponibles. El controlador de MediaServer publica un mensaje en index.js; index.js escucha este mensaje y, cuando se recibe, llama a Upnp.js.

Upnp library usa la API de socket de la app de Chrome para conectar la app de reproductor multimedia con cualquier servidor multimedia descubierto y recibir datos multimedia de este. Upnp.js también usa soapclient.js para analizar los datos del servidor de medios. En el resto de esta sección, se describe este flujo de trabajo con más detalle.

Publicar mensaje

Cuando un usuario hace clic en el botón Servidores de contenido multimedia en el centro de la app de reproductor multimedia, MediaServers.js llama a discoverServers(). Esta función primero verifica si hay solicitudes de descubrimiento pendientes y, si es verdadera, las anula para que se pueda iniciar la solicitud nueva. A continuación, el controlador publica un mensaje en index.js con una clave upnp-discovery y dos objetos de escucha de devolución de llamada:

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

Cómo llamar a upnpDiscover()

index.js escucha el mensaje "upnp-discover" de app.js y responde llamando a upnpDiscover(). Cuando se detecta un servidor multimedia, index.js extrae el dominio del servidor multimedia de los parámetros, guarda el servidor de forma local, da formato a los datos del servidor multimedia y envía los datos al controlador MediaServer.

Cómo analizar los datos del servidor de medios

Cuando Upnp.js descubre un servidor multimedia nuevo, recupera una descripción del dispositivo y envía una Soaprequest para explorar y analizar los datos del servidor de contenido multimedia. soapclient.js analiza los elementos multimedia por nombre de etiqueta en un documento.

Conectarse al servidor multimedia

Upnp.js se conecta a los servidores de medios detectados y recibe datos multimedia mediante la API de socket de la app de Chrome:

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

Explora y reproduce contenido multimedia

El controlador MediaExplorer enumera todos los archivos multimedia dentro de una carpeta del servidor multimedia y es responsable de actualizar la navegación de la ruta de navegación en la ventana de la app del reproductor multimedia. Cuando un usuario selecciona un archivo multimedia, el control publica un mensaje en index.js con la tecla "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 escucha el mensaje de la publicación y responde llamando a 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);
}

Guardar contenido multimedia sin conexión

La mayor parte del trabajo duro para guardar contenido multimedia sin conexión lo realiza la biblioteca filer.js. Puedes obtener más información sobre esta biblioteca en Introducción a filer.js.

El proceso se inicia cuando un usuario selecciona uno o más archivos e inicia la acción "Sin conexión". El controlador MediaExplorer publica un mensaje en index.js con una clave "download-media"; index.js escucha este mensaje y llama a la función downloadMedia() para iniciar el proceso de descarga:

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

El método de utilidad DownloadProcess crea una solicitud xhr para obtener datos del servidor multimedia y espera el estado de finalización. Esto inicia la devolución de llamada de carga que verifica el contenido recibido y guarda los datos de forma local con la función 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);
    }
);

Cuando finaliza el proceso de descarga, MediaExplorer actualiza la lista de archivos multimedia y el panel del árbol del reproductor multimedia.