Criar apps com o Sencha Ext JS

O objetivo deste documento é ajudar você a começar a criar apps do Chrome com o Sencha Ext JS de análise de dados em nuvem. Para alcançar esse objetivo, vamos nos aprofundar em um app de player de mídia criado por Sencha. A origem e a documentação da API estão disponíveis no GitHub.

Esse app descobre os servidores de mídia disponíveis do usuário, incluindo dispositivos de mídia conectados ao PC e um software que gerencia mídia pela rede. Os usuários podem procurar mídia, abrir na rede ou salvar off-line.

Aqui estão as principais coisas que você precisa fazer para criar um aplicativo de player de mídia usando o Sencha Ext JS:

  • Criar o manifesto, manifest.json.
  • Crie uma página de evento: background.js.
  • Lógica do app Sandbox.
  • Fazer uma comunicação entre o app do Chrome e os arquivos no sandbox
  • Descobrir servidores de mídia.
  • Conheça e acesse conteúdo de mídia.
  • Salvar mídia off-line.

Criar manifesto

Todos os apps do Chrome exigem um arquivo de manifesto com as informações que o Chrome precisa para iniciar. apps. Conforme indicado no manifesto, o app player de mídia é "offline_enabled"; os recursos de mídia podem ser salvos localmente, acessados e reproduzidos independentemente da conectividade.

O "sandbox" é usado para colocar a lógica principal do aplicativo no sandbox em uma origem exclusiva. Todas no sandbox conteúdo está isento da Política de Segurança de Conteúdo de apps do Chrome, mas não pode acessar diretamente o APIs de aplicativos do Google Chrome. O manifesto também inclui o valor-chave permissão; o app player de mídia usa o API socket para se conectar a um servidor de mídia pela rede.

{
    "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 "Criar evento"

Todos os apps do Chrome exigem que o background.js inicie o app. A página principal do player de mídia, index.html, abre em uma janela com as dimensões 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 do aplicativo sandbox

Os apps do Google Chrome são executados em um ambiente controlado que aplica uma Política de Segurança de Conteúdo rigorosa (CSP). O app de reprodução de mídia precisa de alguns privilégios mais altos para renderizar os componentes Ext JS. Para obedecer à CSP e executar a lógica do app, a página principal index.html vai criar um iframe que atua como um ambiente de sandbox:

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

O iframe aponta para sandbox.html, que inclui os arquivos necessários para a extensão JS aplicativo:

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

O script app.js executa todo o código JS Ext e renderiza as visualizações do player de mídia. Como este script está no modo sandbox, não pode acessar diretamente as APIs do aplicativo do Google Chrome. Comunicação entre app.js e arquivos sem sandbox é feito com a API HTML5 Post Message.

Comunicação entre arquivos

Para que o app de player de mídia acesse as APIs de apps do Chrome, como consultar a rede em busca de mídia servidores, app.js publica mensagens em index.js. Ao contrário do app.js no modo sandbox, o index.js pode acessar diretamente as APIs de aplicativos do Google Chrome.

index.js cria o iframe:

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

iframeWindow = iframe.contentWindow;

E detecta mensagens dos arquivos no sandbox:

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

No exemplo a seguir, app.js envia uma mensagem para index.js solicitando a chave "extension-baseurl":

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

index.js recebe a solicitação, atribui o resultado e responde enviando o URL de base de volta:

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

Descobrir servidores de mídia

A descoberta dos servidores de mídia envolve muitos fatores. De modo geral, o fluxo de trabalho de descoberta iniciado por uma ação do usuário para pesquisar os servidores de mídia disponíveis. O controlador do MediaServer (link em inglês) posta uma mensagem no index.js; O app index.js vai ouvir esta mensagem e, quando for recebida, as ligações Upnp.js:

O Upnp library usa a API de soquete do app Chrome para conectar o app de player de mídia a qualquer servidores de mídia descobertos e receber dados de mídia do servidor de mídia. O Upnp.js também usa soapclient.js para analisar os dados do servidor de mídia. No restante desta seção, descrevemos fluxo de trabalho em mais detalhes.

Postar mensagem

Quando um usuário clica no botão "Servidores de mídia" no centro do app de player de mídia, MediaServers.js chama discoverServers(). Primeiro, essa função verifica se há solicitações de descoberta pendentes e se true, as cancela para que a nova solicitação possa ser iniciada. Em seguida, o controlador publica uma mensagem no index.js com uma chave upnp-discovery e dois listeners de callback:

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

Chamar upnpDiscover()

index.js detecta "upnp-discover" de app.js e responde chamando upnpDiscover() Quando um servidor de mídia é descoberto, o index.js extrai o domínio do servidor de mídia dos parâmetros, salva o servidor localmente, formata os dados do servidor de mídia e os envia para o controlador MediaServer.

Analisar dados do servidor de mídia

Quando o Upnp.js descobre um novo servidor de mídia, ele recupera uma descrição do dispositivo e envia uma solicitação Soap para navegar e analisar os dados do servidor de mídia; soapclient.js analisa os elementos de mídia por nome de tag em um documento.

Conectar ao servidor de mídia

O Upnp.js se conecta aos servidores de mídia descobertos e recebe dados de mídia usando o soquete do app Chrome API:

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

Acessar e abrir mídia

O controlador MediaExplorer lista todos os arquivos de mídia dentro de uma pasta do servidor de mídia e é responsável por atualizar a navegação estrutural na janela do app player de mídia. Quando um usuário seleciona um arquivo de mídia, o controle posta uma mensagem no index.js com a tag "play-media" chave:

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 detecta a mensagem da postagem e responde chamando 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);
}

Salvar mídia off-line

A maior parte do trabalho para salvar mídia off-line é feita pela biblioteca filer.js. Saiba mais essa biblioteca em Introdução ao filer.js.

O processo é iniciado quando um usuário seleciona um ou mais arquivos e inicia a ação "Ficar off-line" à ação. O controlador do MediaExplorer (link em inglês) posta uma mensagem para index.js com a chave "download-media". index.js detecta essa mensagem e chama a função downloadMedia() para iniciar o processo de download:

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

O método utilitário DownloadProcess cria uma solicitação xhr para receber dados do servidor de mídia e aguarda o status de conclusão. Isso inicia o callback onload, que verifica o conteúdo recebido e salva os dados localmente usando a função 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);
    }
);

Quando o processo de download for concluído, o MediaExplorer vai atualizar a lista de arquivos de mídia e a mídia painel de árvore do player.