Het doel van dit document is om u op weg te helpen bij het bouwen van Chrome-apps met het Sencha Ext JS- framework. Om dit doel te bereiken duiken we in een mediaspeler-app gebouwd door Sencha. De broncode en API-documentatie zijn beschikbaar op GitHub.
Deze app ontdekt de beschikbare mediaservers van een gebruiker, inclusief media-apparaten die op de pc zijn aangesloten en software die media via het netwerk beheert. Gebruikers kunnen door media bladeren, via het netwerk afspelen of offline opslaan.
Hier zijn de belangrijkste dingen die u moet doen om een mediaspeler-app te bouwen met Sencha Ext JS:
- Maak manifest,
manifest.json
. - Maak een evenementenpagina ,
background.js
. - Logica van de Sandbox- app.
- Communiceer tussen de Chrome-app en bestanden in de sandbox.
- Ontdek mediaservers.
- Media verkennen en afspelen.
- Bewaar media offline.
Maak een manifest
Voor alle Chrome-apps is een manifestbestand vereist dat de informatie bevat die Chrome nodig heeft om apps te starten. Zoals aangegeven in het manifest is de mediaspeler-app "offline_enabled"; media-items kunnen lokaal worden opgeslagen, geopend en afgespeeld, ongeacht de connectiviteit.
Het veld 'sandbox' wordt gebruikt om de hoofdlogica van de app in een unieke oorsprong te sandboxen. Alle inhoud in een sandbox is vrijgesteld van het Chrome App Content Security Policy , maar heeft geen directe toegang tot de Chrome App API's. Het manifest bevat ook de machtiging 'socket'; de mediaspeler-app gebruikt de socket API om via het netwerk verbinding te maken met een mediaserver.
{
"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"
]
}
]
}
Maak een evenementenpagina
Alle Chrome-apps hebben background.js
nodig om de applicatie te starten. De hoofdpagina van de mediaspeler, index.html
, wordt geopend in een venster met de opgegeven afmetingen:
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;
});
});
Logica van de Sandbox-app
Chrome-apps worden uitgevoerd in een gecontroleerde omgeving die een strikt Content Security Policy (CSP) afdwingt. De mediaspeler-app heeft wat hogere rechten nodig om de Ext JS-componenten weer te geven. Om te voldoen aan CSP en de app-logica uit te voeren, maakt de hoofdpagina van de app, index.html
, een iframe dat fungeert als een sandbox-omgeving:
<iframe id="sandbox-frame" sandbox="allow-scripts" src="sandbox.html"></iframe>
Het iframe verwijst naar sandbox.html , dat de bestanden bevat die nodig zijn voor de Ext JS-applicatie:
<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>
Het app.js- script voert alle Ext JS-code uit en geeft de weergaven van de mediaspeler weer. Omdat dit script in een sandbox is geplaatst, heeft het geen rechtstreekse toegang tot de API's van de Chrome-app. Communicatie tussen app.js
en niet-sandboxbestanden vindt plaats met behulp van de HTML5 Post Message API .
Communiceer tussen bestanden
Om ervoor te zorgen dat de mediaspeler-app toegang krijgt tot de API's van de Chrome-app, zoals het netwerk doorzoekt naar mediaservers, plaatst app.js
berichten naar index.js . In tegenstelling tot app.js
in de sandbox heeft index.js
rechtstreeks toegang tot de API's van de Chrome-app.
index.js
maakt het iframe:
var iframe = document.getElementById('sandbox-frame');
iframeWindow = iframe.contentWindow;
En luistert naar berichten uit de sandboxbestanden:
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);
In het volgende voorbeeld stuurt app.js
een bericht naar index.js
met het verzoek om de sleutel 'extension-baseurl':
Ext.data.PostMessage.request({
key: 'extension-baseurl',
success: function(data) {
//...
}
});
index.js
ontvangt het verzoek, wijst het resultaat toe en antwoordt door de basis-URL terug te sturen:
function extensionBaseUrl(data) {
data.result = chrome.extension.getURL('/');
iframeWindow.postMessage(data, '*');
}
Ontdek mediaservers
Er komt veel kijken bij het ontdekken van mediaservers. Op een hoog niveau wordt de ontdekkingsworkflow geïnitieerd door een gebruikersactie om naar beschikbare mediaservers te zoeken. De MediaServer-controller plaatst een bericht op index.js
; index.js
luistert naar dit bericht en roept bij ontvangst Upnp.js aan.
De Upnp library
gebruikt de Chrome App socket API om de mediaspeler-app te verbinden met eventueel ontdekte mediaservers en mediagegevens van de mediaserver te ontvangen. Upnp.js
gebruikt ook soapclient.js om de mediaservergegevens te parseren. In de rest van deze sectie wordt deze workflow gedetailleerder beschreven.
Bericht plaatsen
Wanneer een gebruiker op de knop Media Servers in het midden van de mediaspeler-app klikt, roept MediaServers.js
discoverServers()
aan. Deze functie controleert eerst of er nog openstaande detectieverzoeken zijn en als dit waar is, worden deze afgebroken zodat het nieuwe verzoek kan worden gestart. Vervolgens plaatst de controller een bericht naar index.js
met een sleutel upnp-discovery en twee callback-listeners:
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');
}
});
Roep npDiscover() op
index.js
luistert naar het 'upnp-discover'-bericht van app.js
en reageert door upnpDiscover()
aan te roepen. Wanneer een mediaserver wordt ontdekt, extraheert index.js
het mediaserverdomein uit de parameters, slaat de server lokaal op, formatteert de mediaservergegevens en pusht de gegevens naar de MediaServer
controller.
Parseer mediaservergegevens
Wanneer Upnp.js
een nieuwe mediaserver ontdekt, haalt het een beschrijving van het apparaat op en verzendt het een Soaprequest om door de mediaservergegevens te bladeren en deze te parseren; soapclient.js
parseert de media-elementen op tagnaam in een document.
Maak verbinding met de mediaserver
Upnp.js
maakt verbinding met ontdekte mediaservers en ontvangt mediagegevens met behulp van de Chrome App socket 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);
});
});
});
Media verkennen en afspelen
De MediaExplorer-controller geeft een overzicht van alle mediabestanden in een mediaservermap en is verantwoordelijk voor het bijwerken van de broodkruimelnavigatie in het app-venster van de mediaspeler. Wanneer een gebruiker een mediabestand selecteert, plaatst de controller een bericht op index.js
met de 'play-media'-toets:
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
luistert naar dit bericht en reageert door playMedia()
aan te roepen:
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);
}
Bewaar media offline
Het meeste werk om media offline op te slaan wordt gedaan door de filer.js-bibliotheek . U kunt meer over deze bibliotheek lezen in Introductie van filer.js .
Het proces start wanneer een gebruiker een of meer bestanden selecteert en de actie 'Offline nemen' initieert. De MediaExplorer-controller plaatst een bericht op index.js
met de sleutel 'download-media'; index.js
luistert naar dit bericht en roept de downloadMedia()
-functie aan om het downloadproces te starten:
function downloadMedia(data) {
DownloadProcess.run(data.params.files, function() {
data.result = true;
sendMessage(data);
});
}
De DownloadProcess
hulpprogrammamethode creëert een xhr-verzoek om gegevens van de mediaserver op te halen en wacht op de voltooiingsstatus. Dit initieert de onload callback die de ontvangen inhoud controleert en de gegevens lokaal opslaat met behulp van de filer.js
functie:
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);
}
);
Wanneer het downloadproces is voltooid, werkt MediaExplorer
de lijst met mediabestanden en het boompaneel van de mediaspeler bij.