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.