blob: de4ab9dbf03bf5b1374da222524af3214b24d523 [file] [log] [blame]
<meta name="doc-family" content="apps">
<h1>Build Apps with Sencha Ext JS</h1>
<p>
The goal of this doc is to get you started
on building packaged apps with the
<a href="http://www.sencha.com/products/extjs">Sencha Ext JS</a> framework.
To achieve this goal,
we will dive into a media player app built by Sencha.
The <a href="https://github.com/GoogleChrome/sencha-video-player-app">source code</a>
and <a href="http://senchaprosvcs.github.com/GooglePlayer/docs/output/#!/api">API Documentation</a> are available on GitHub.
</p>
<p>
This app discovers a user's available media servers,
including media devices connected to the pc and
software that manages media over the network.
Users can browse media, play over the network,
or save offline.
</p>
<p>Here are the key things you must do
to build a media player app using Sencha Ext JS:
</p>
<ul>
<li>Create manifest, <code>manifest.json</code>.</li>
<li>Create <a href="app_lifecycle.html#eventpage">event page</a>,
<code>background.js</code>.</li>
<li><a href="app_external.html#sandboxing">Sandbox</a> app's logic.</li>
<li>Communicate between packaged app and sandboxed files.</li>
<li>Discover media servers.</li>
<li>Explore and play media.</li>
<li>Save media offline.</li>
</ul>
<h2 id="first">Create manifest</h2>
<p>
All packaged apps require a
<a href="manifest.html">manifest file</a>
which contains the information Chrome needs to launch apps.
As indicated in the manifest,
the media player app is "offline_enabled";
media assets can be saved locally,
accessed and played regardless of connectivity.
</p>
<p>
The "sandbox" field is used
to sandbox the app's main logic in a unique origin.
All sandboxed content is exempt from the packaged app
<a href="contentSecurityPolicy.html">Content Security Policy</a>,
but cannot directly access the packaged app APIs.
The manifest also includes the "socket" permission;
the media player app uses the <a href="socket.html">socket API</a>
to connect to a media server over the network.
</p>
<pre>
{
"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"
]
}
]
}
</pre>
<h2 id="second">Create event page</h2>
<p>
All packaged apps require <code>background.js</code>
to launch the application.
The media player's main page, <code>index.html</code>,
opens in a window with the specified dimensions:
</p>
<pre>
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;
});
});
</pre>
<h2 id="three">Sandbox app's logic</h2>
<p>Packaged apps run in a controlled environment
that enforces a strict <a href="contentSecurityPolicy.html">Content Security Policy (CSP)</a>.
The media player app needs some higher privileges to render the Ext JS components.
To comply with CSP and execute the app logic,
the app's main page, <code>index.html</code>, creates an iframe
that acts as a sandbox environment:
<pre>
&lt;iframe id="sandbox-frame" class="sandboxed" sandbox="allow-scripts" src="sandbox.html">&lt;/iframe>
</pre>
<p>The iframe points to <a href="https://github.com/GoogleChrome/sencha-video-player-app/blob/master/sandbox.html">sandbox.html</a> which includes the files required for the Ext JS application:
</p>
<pre>
&lt;html>
&lt;head>
&lt;link rel="stylesheet" type="text/css" href="resources/css/app.css" />'
&lt;script src="sdk/ext-all-dev.js">&lt;/script>'
&lt;script src="lib/ext/data/PostMessage.js">&lt;/script>'
&lt;script src="lib/ChromeProxy.js">&lt;/script>'
&lt;script src="app.js">&lt;/script>
&lt;/head>
&lt;body>&lt;/body>
&lt;/html>
</pre>
<p>
The <a href="http://senchaprosvcs.github.com/GooglePlayer/docs/output/source/app.html#VP-Application">app.js</a> script executes all the Ext JS code and renders the media player views.
Since this script is sandboxed, it cannot directly access the packaged app APIs.
Communication between <code>app.js</code> and non-sandboxed files is done using the
<a href="https://developer.mozilla.org/en-US/docs/DOM/window.postMessage">HTML5 Post Message API</a>.
</p>
<h2 id="four">Communicate between files</h2>
<p>
In order for the media player app to access packaged app APIs,
like query the network for media servers, <code>app.js</code> posts messages
to <a href="https://github.com/GoogleChrome/sencha-video-player-app/blob/master/index.js">index.js</a>.
Unlike the sandboxed <code>app.js</code>,
<code>index.js</code> can directly access the packaged app APIs.
</p>
<p>
<code>index.js</code> creates the iframe:
</p>
<pre>
var iframe = document.getElementById('sandbox-frame');
iframeWindow = iframe.contentWindow;
</pre>
<p>
And listens for messages from the sandboxed files:
</p>
<pre>
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);
</pre>
<p>
In the following example,
<code>app.js</code> sends a message to <code>index.js</code>
requesting the key 'extension-baseurl':
</p>
<pre>
Ext.data.PostMessage.request({
key: 'extension-baseurl',
success: function(data) {
//...
}
});
</pre>
<p>
<code>index.js</code> receives the request, assigns the result,
and replies by sending the Base URL back:
</p>
<pre>
function extensionBaseUrl(data) {
data.result = chrome.extension.getURL('/');
iframeWindow.postMessage(data, '*');
}
</pre>
<h2 id="five">Discover media servers</h2>
<p>
There's a lot that goes into discovering media servers.
At a high level, the discovery workflow is initiated
by a user action to search for available media servers.
The <a href="https://github.com/GoogleChrome/sencha-video-player-app/blob/master/app/controller/MediaServers.js">MediaServer controller</a>
posts a message to <code>index.js</code>;
<code>index.js</code> listens for this message and when received,
calls <a href="https://github.com/GoogleChrome/sencha-video-player-app/blob/master/lib/Upnp.js">Upnp.js</a>.
</p>
<p>
The <code>Upnp library</code> uses the packaged app
<a href="app_network.html">socket API</a>
to connect the media player app with any discovered media servers
and receive media data from the media server.
<code>Upnp.js</code> also uses
<a href="https://github.com/GoogleChrome/sencha-video-player-app/blob/master/lib/soapclient.js">soapclient.js</a>
to parse the media server data.
The remainder of this section describes this workflow in more detail.
</p>
<h3 id="post">Post message</h3>
<p>
When a user clicks the Media Servers button in the center of the media player app,
<code>MediaServers</code> calls <code>discoverServers()</code>.
This function first checks for any outstanding discovery requests,
and if true, aborts them so the new request can be initiated.
Next, the controller posts a message to <code>index.js</code>
with a key upnp-discovery, and two callback listeners:
</p>
<pre>
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');
}
});
</pre>
<h3 id="call">Call upnpDiscover()</h3>
<p>
<code>index.js</code> listens
for the 'upnp-discover' message from <code>app.js</code>
and responds by calling <code>upnpDiscover()</code>.
When a media server is discovered,
<code>index.js</code> extracts the media server domain from the parameters,
saves the server locally, formats the media server data,
and pushes the data to the <code>MediaServer</code> controller.
</p>
<h3 id="parse">Parse media server data</h3>
<p>
When <code>Upnp.js</code> discovers a new media server,
it then retrieves a description of the device
and sends a Soaprequest to browse and parse the media server data;
<code>soapclient.js</code> parses the media elements by tag name
into a document.
</p>
<h3 id="connect">Connect to media server</h3>
<p>
<code>Upnp.js</code> connects to discovered media servers
and receives media data using the packaged app socket API:
</p>
<pre>
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);
});
});
});
</pre>
<h2 id="six">Explore and play media</h2>
<p>
The
<a href="https://github.com/GoogleChrome/sencha-video-player-app/blob/master/app/controller/MediaExplorer.js">MediaExplorer controller</a>
lists all the media files inside a media server folder
and is responsible for updating the breadcrumb navigation
in the media player app window.
When a user selects a media file,
the controller posts a message to <code>index.js</code>
with the 'play-media' key:
</p>
<pre>
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
}
});
}
},
</pre>
<p>
<code>index.js</code> listens for this post message and
responds by calling <code>playMedia()</code>:
</p>
<pre>
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( 'webkitTransitionEnd', animEnd, false );
animEnd = null;
};
//load media
mediaEl.src = url;
mediaEl.load();
//animate in player
playerCt.addEventListener( 'webkitTransitionEnd', animEnd, false );
playerCt.style.webkitTransform = "translateY(0)";
//reply postmessage
data.result = true;
sendMessage(data);
}
</pre>
<h2 id="seven">Save media offline</h2>
<p>
Most of the hard work to save media offline is done by the
<a href="https://github.com/GoogleChrome/sencha-video-player-app/blob/master/lib/filer.js">filer.js library</a>.
You can read more this library in
<a href="http://ericbidelman.tumblr.com/post/14866798359/introducing-filer-js">Introducing filer.js</a>.
</p>
<p>
The process kicks off when a user selects one or more files
and initiates the 'Take offline' action.
The
<a href="https://github.com/GoogleChrome/sencha-video-player-app/blob/master/app/controller/MediaExplorer.js">MediaExplorer controller</a> posts a message to <code>index.js</code>
with a key 'download-media'; <code>index.js</code> listens for this message
and calls the <code>downloadMedia()</code> function
to initiate the download process:
</p>
<pre>
function downloadMedia(data) {
DownloadProcess.run(data.params.files, function() {
data.result = true;
sendMessage(data);
});
}
</pre>
<p>
The <code>DownloadProcess</code> utility method creates an xhr request
to get data from the media server and waits for completion status.
This initiates the onload callback which checks the received content
and saves the data locally using the <code>filer.js</code> function:
</p>
<pre>
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);
}
);
</pre>
<p>
When the download process is finished,
<code>MediaExplorer</code> updates the media file list and the media player tree panel.
</p>
<p class="backtotop"><a href="#top">Back to top</a></p>