Criar apps com o Sencha Ext JS

O objetivo deste documento é ajudar você a começar a criar apps do Chrome com o framework Sencha Ext JS. Para isso, vamos detalhar o app de player de mídia criado pelo Sencha. O código fonte e a documentação da API (links em inglês) estão disponíveis no GitHub.

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

Confira as principais etapas para criar um app de player de mídia usando o Sencha Ext JS:

  • Crie o manifesto, manifest.json.
  • Crie uma página de evento, background.js.
  • Lógica do app Sandbox.
  • Comunicação entre o app do Chrome e arquivos no modo sandbox.
  • Descobrir servidores de mídia.
  • Explore e reproduza mídia.
  • Salvar mídia off-line.

Criar manifesto

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

O campo "sandbox" é usado para colocar a lógica principal do aplicativo no sandbox em uma origem exclusiva. Todo o conteúdo no modo sandbox está isento da Política de Segurança de Conteúdo de apps do Chrome, mas não pode acessar diretamente as APIs de apps do Chrome. O manifesto também inclui a permissão "soquete". O app de player de mídia usa a API do soquete 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"
            ]
        }
    ]
}

Criar página de evento

Todos os apps do Chrome exigem o background.js para iniciar o app. A página principal do player de mídia, index.html, é aberta 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 app no sandbox

Os apps do Chrome são executados em um ambiente controlado que aplica uma Política de Segurança de Conteúdo (CSP) rigorosa. O app do player 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, cria 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 o aplicativo 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>

O script app.js executa todo o código Ext JS e renderiza as visualizações do player de mídia. Como esse script está no sandbox, ele não pode acessar diretamente as APIs do app Chrome. A comunicação entre arquivos app.js e que não estão em sandbox é feita usando a API HTML5 Post Message.

Comunicação entre arquivos

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

index.js cria o iframe:

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

iframeWindow = iframe.contentWindow;

E detecta mensagens de arquivos no modo 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

Descobrir servidores de mídia exige muita coisa. Em um nível alto, o fluxo de trabalho de descoberta é iniciado por uma ação do usuário para procurar servidores de mídia disponíveis. O controlador MediaServer (link em inglês) posta uma mensagem em index.js. index.js detecta essa mensagem e, quando recebida, chama Upnp.js.

O Upnp library usa a API Socket do app Chrome para conectar o app de player de mídia a qualquer servidor de mídia descoberto e receber dados de mídia dele. Upnp.js também usa soapclient.js para analisar os dados do servidor de mídia. No restante desta seção, descrevemos esse 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(). Essa função verifica primeiro se há solicitações de descoberta pendentes e, se verdadeira, cancela-as para que a nova solicitação possa ser iniciada. Em seguida, o controlador publica uma mensagem para 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 a mensagem "upnp-discover" do 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 dele e os envia para o controlador MediaServer.

Analisar dados do servidor de mídia

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

Conectar ao servidor de mídia

O Upnp.js se conecta a servidores de mídia descobertos e recebe dados de mídia usando a 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);
        });
    });
});

Explorar e reproduzir mídia

O controlador do 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 na localização atual na janela do app do player de mídia. Quando um usuário seleciona um arquivo de mídia, o controlador publica uma mensagem em index.js com a chave "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
            }
        });
    }
},

O index.js detecta essa mensagem de 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 duro para salvar mídia off-line é feita pela biblioteca filer.js. Leia mais sobre 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 "Fazer off-line". O controlador MediaExplorer publica uma mensagem para index.js com uma 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 ao carregar 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 é concluído, o MediaExplorer atualiza a lista de arquivos de mídia e o painel de árvore do player de mídia.