1 <meta name=
"doc-family" content=
"apps">
2 <h1>Build Apps with Sencha Ext JS
</h1>
5 The goal of this doc is to get you started
6 on building Chrome Apps with the
7 <a href=
"http://www.sencha.com/products/extjs">Sencha Ext JS
</a> framework.
9 we will dive into a media player app built by Sencha.
10 The
<a href=
"https://github.com/GoogleChrome/sencha-video-player-app">source code
</a>
11 and
<a href=
"http://senchaprosvcs.github.com/GooglePlayer/docs/output/#!/api">API Documentation
</a> are available on GitHub.
15 This app discovers a user's available media servers,
16 including media devices connected to the pc and
17 software that manages media over the network.
18 Users can browse media, play over the network,
22 <p>Here are the key things you must do
23 to build a media player app using Sencha Ext JS:
27 <li>Create manifest,
<code>manifest.json
</code>.
</li>
28 <li>Create
<a href=
"app_lifecycle#eventpage">event page
</a>,
29 <code>background.js
</code>.
</li>
30 <li><a href=
"app_external#sandboxing">Sandbox
</a> app's logic.
</li>
31 <li>Communicate between Chrome App and sandboxed files.
</li>
32 <li>Discover media servers.
</li>
33 <li>Explore and play media.
</li>
34 <li>Save media offline.
</li>
37 <h2 id=
"first">Create manifest
</h2>
40 All Chrome Apps require a
41 <a href=
"manifest">manifest file
</a>
42 which contains the information Chrome needs to launch apps.
43 As indicated in the manifest,
44 the media player app is
"offline_enabled";
45 media assets can be saved locally,
46 accessed and played regardless of connectivity.
50 The
"sandbox" field is used
51 to sandbox the app's main logic in a unique origin.
52 All sandboxed content is exempt from the Chrome App
53 <a href=
"contentSecurityPolicy">Content Security Policy
</a>,
54 but cannot directly access the Chrome App APIs.
55 The manifest also includes the
"socket" permission;
56 the media player app uses the
<a href=
"socket">socket API
</a>
57 to connect to a media server over the network.
60 <pre data-filename=
"manifest.json">
62 "name":
"Video Player",
63 "description":
"Features network media discovery and playlist management",
65 "manifest_version":
2,
66 "offline_enabled": true,
77 "pages": [
"sandbox.html"]
94 <h2 id=
"second">Create event page
</h2>
97 All Chrome Apps require
<code>background.js
</code>
98 to launch the application.
99 The media player's main page,
<code>index.html
</code>,
100 opens in a window with the specified dimensions:
103 <pre data-filename=
"background.js">
104 chrome.app.runtime.onLaunched.addListener(function(launchData) {
110 chrome.app.window.create('index.html', opt, function (win) {
111 win.launchData = launchData;
117 <h2 id=
"three">Sandbox app's logic
</h2>
119 <p>Chrome Apps run in a controlled environment
120 that enforces a strict
<a href=
"contentSecurityPolicy">Content Security Policy (CSP)
</a>.
121 The media player app needs some higher privileges to render the Ext JS components.
122 To comply with CSP and execute the app logic,
123 the app's main page,
<code>index.html
</code>, creates an iframe
124 that acts as a sandbox environment:
126 <pre data-filename=
"index.html">
127 <iframe
id=
"sandbox-frame" sandbox=
"allow-scripts" src=
"sandbox.html"></iframe
>
130 <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:
133 <pre data-filename=
"sandbox.html">
136 <link
rel=
"stylesheet" type=
"text/css" href=
"resources/css/app.css" />'
137 <script
src=
"sdk/ext-all-dev.js"></script
>'
138 <script
src=
"lib/ext/data/PostMessage.js"></script
>'
139 <script
src=
"lib/ChromeProxy.js"></script
>'
140 <script
src=
"app.js"></script
>
147 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.
148 Since this script is sandboxed, it cannot directly access the Chrome App APIs.
149 Communication between
<code>app.js
</code> and non-sandboxed files is done using the
150 <a href=
"https://developer.mozilla.org/en-US/docs/DOM/window.postMessage">HTML5 Post Message API
</a>.
153 <h2 id=
"four">Communicate between files
</h2>
156 In order for the media player app to access Chrome App APIs,
157 like query the network for media servers,
<code>app.js
</code> posts messages
158 to
<a href=
"https://github.com/GoogleChrome/sencha-video-player-app/blob/master/index.js">index.js
</a>.
159 Unlike the sandboxed
<code>app.js
</code>,
160 <code>index.js
</code> can directly access the Chrome App APIs.
164 <code>index.js
</code> creates the iframe:
167 <pre data-filename=
"index.js">
168 var iframe = document.getElementById('sandbox-frame');
170 iframeWindow = iframe.contentWindow;
174 And listens for messages from the sandboxed files:
177 <pre data-filename=
"index.js">
178 window.addEventListener('message', function(e) {
182 console.log('[index.js] Post Message received with key ' + key);
185 case 'extension-baseurl':
186 extensionBaseUrl(data);
189 case 'upnp-discover':
201 case 'download-media':
205 case 'cancel-download':
206 cancelDownload(data);
210 console.log('[index.js] unidentified key for Post Message:
"' + key + '"');
216 In the following example,
217 <code>app.js
</code> sends a message to
<code>index.js
</code>
218 requesting the key 'extension-baseurl':
221 <pre data-filename=
"app.js">
222 Ext.data.PostMessage.request({
223 key: 'extension-baseurl',
224 success: function(data) {
231 <code>index.js
</code> receives the request, assigns the result,
232 and replies by sending the Base URL back:
235 <pre data-filename=
"index.js">
236 function extensionBaseUrl(data) {
237 data.result = chrome.extension.getURL('/');
238 iframeWindow.postMessage(data, '*');
242 <h2 id=
"five">Discover media servers
</h2>
245 There's a lot that goes into discovering media servers.
246 At a high level, the discovery workflow is initiated
247 by a user action to search for available media servers.
248 The
<a href=
"https://github.com/GoogleChrome/sencha-video-player-app/blob/master/app/controller/MediaServers.js">MediaServer controller
</a>
249 posts a message to
<code>index.js
</code>;
250 <code>index.js
</code> listens for this message and when received,
251 calls
<a href=
"https://github.com/GoogleChrome/sencha-video-player-app/blob/master/lib/Upnp.js">Upnp.js
</a>.
255 The
<code>Upnp library
</code> uses the Chrome App
256 <a href=
"app_network">socket API
</a>
257 to connect the media player app with any discovered media servers
258 and receive media data from the media server.
259 <code>Upnp.js
</code> also uses
260 <a href=
"https://github.com/GoogleChrome/sencha-video-player-app/blob/master/lib/soapclient.js">soapclient.js
</a>
261 to parse the media server data.
262 The remainder of this section describes this workflow in more detail.
265 <h3 id=
"post">Post message
</h3>
268 When a user clicks the Media Servers button in the center of the media player app,
269 <code>MediaServers.js
</code> calls
<code>discoverServers()
</code>.
270 This function first checks for any outstanding discovery requests,
271 and if true, aborts them so the new request can be initiated.
272 Next, the controller posts a message to
<code>index.js
</code>
273 with a key upnp-discovery, and two callback listeners:
276 <pre data-filename=
"MediaServers.js">
277 me.activeDiscoverRequest = Ext.data.PostMessage.request({
278 key: 'upnp-discover',
279 success: function(data) {
281 delete me.activeDiscoverRequest;
283 if (serversGraph.isDestroyed) {
287 mainBtn.isLoading = false;
288 mainBtn.removeCls('pop-in');
289 mainBtn.setIconCls('ico-server');
290 mainBtn.setText('Media Servers');
293 Ext.each(data, function(server) {
295 urlBase = server.urlBase;
298 if (urlBase.substr(urlBase.length-
1,
1) === '/'){
299 urlBase = urlBase.substr(
0, urlBase.length-
1);
303 if (server.icons && server.icons.length) {
304 if (server.icons[
1]) {
305 icon = server.icons[
1].url;
308 icon = server.icons[
0].url;
311 icon = urlBase + icon;
316 text: server.friendlyName,
324 failure: function() {
325 delete me.activeDiscoverRequest;
327 if (serversGraph.isDestroyed) {
331 mainBtn.isLoading = false;
332 mainBtn.removeCls('pop-in');
333 mainBtn.setIconCls('ico-error');
334 mainBtn.setText('Error...click to retry');
339 <h3 id=
"call">Call upnpDiscover()
</h3>
342 <code>index.js
</code> listens
343 for the 'upnp-discover' message from
<code>app.js
</code>
344 and responds by calling
<code>upnpDiscover()
</code>.
345 When a media server is discovered,
346 <code>index.js
</code> extracts the media server domain from the parameters,
347 saves the server locally, formats the media server data,
348 and pushes the data to the
<code>MediaServer
</code> controller.
351 <h3 id=
"parse">Parse media server data
</h3>
354 When
<code>Upnp.js
</code> discovers a new media server,
355 it then retrieves a description of the device
356 and sends a Soaprequest to browse and parse the media server data;
357 <code>soapclient.js
</code> parses the media elements by tag name
361 <h3 id=
"connect">Connect to media server
</h3>
364 <code>Upnp.js
</code> connects to discovered media servers
365 and receives media data using the Chrome App socket API:
368 <pre data-filename=
"Upnp.js">
369 socket.create(
"udp", {}, function(info) {
370 var socketId = info.socketId;
373 socket.bind(socketId,
"0.0.0.0",
0, function(info) {
376 var message = String.toBuffer(UPNP_MESSAGE);
379 socket.sendTo(socketId, message, UPNP_ADDRESS, UPNP_PORT, function(info) {
382 setTimeout(function() {
385 socket.recvFrom(socketId, function(info) {
388 var data = String.fromBuffer(info.data),
390 locationReg = /^location:/i;
392 //extract location info
394 data = data.split(
"\r\n");
396 data.forEach(function(value) {
397 if (locationReg.test(value)){
398 servers.push(value.replace(locationReg,
"").trim());
414 <h2 id=
"six">Explore and play media
</h2>
418 <a href=
"https://github.com/GoogleChrome/sencha-video-player-app/blob/master/app/controller/MediaExplorer.js">MediaExplorer controller
</a>
419 lists all the media files inside a media server folder
420 and is responsible for updating the breadcrumb navigation
421 in the media player app window.
422 When a user selects a media file,
423 the controller posts a message to
<code>index.js
</code>
424 with the 'play-media' key:
427 <pre data-filename=
"MediaExplorer.js">
428 onFileDblClick: function(explorer, record) {
429 var serverPanel, node,
430 type = record.get('type'),
431 url = record.get('url'),
432 name = record.get('name'),
433 serverId= record.get('serverId');
435 if (type === 'audio' || type === 'video') {
436 Ext.data.PostMessage.request({
449 <code>index.js
</code> listens for this post message and
450 responds by calling
<code>playMedia()
</code>:
453 <pre data-filename=
"index.js">
454 function playMedia(data) {
455 var type = data.params.type,
456 url = data.params.url,
457 playerCt = document.getElementById('player-ct'),
458 audioBody = document.getElementById('audio-body'),
459 videoBody = document.getElementById('video-body'),
460 mediaEl = playerCt.getElementsByTagName(type)[
0],
461 mediaBody = type === 'video' ? videoBody : audioBody,
468 name: data.params.name
472 audioBody.style.display = 'none';
473 videoBody.style.display = 'none';
475 var animEnd = function(e) {
478 mediaBody.style.display = '';
484 playerCt.removeEventListener( 'webkitTransitionEnd', animEnd, false );
493 playerCt.addEventListener( 'webkitTransitionEnd', animEnd, false );
494 playerCt.style.webkitTransform =
"translateY(0)";
502 <h2 id=
"seven">Save media offline
</h2>
505 Most of the hard work to save media offline is done by the
506 <a href=
"https://github.com/GoogleChrome/sencha-video-player-app/blob/master/lib/filer.js">filer.js library
</a>.
507 You can read more this library in
508 <a href=
"http://ericbidelman.tumblr.com/post/14866798359/introducing-filer-js">Introducing filer.js
</a>.
512 The process kicks off when a user selects one or more files
513 and initiates the 'Take offline' action.
515 <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>
516 with a key 'download-media';
<code>index.js
</code> listens for this message
517 and calls the
<code>downloadMedia()
</code> function
518 to initiate the download process:
521 <pre data-filename=
"index.js">
522 function downloadMedia(data) {
523 DownloadProcess.run(data.params.files, function() {
531 The
<code>DownloadProcess
</code> utility method creates an xhr request
532 to get data from the media server and waits for completion status.
533 This initiates the onload callback which checks the received content
534 and saves the data locally using the
<code>filer.js
</code> function:
537 <pre data-filename=
"filer.js">
541 data: Util.arrayBufferToBlob(fileArrayBuf),
544 function(fileEntry, fileWriter) {
546 console.log('file saved!');
548 //increment downloaded
551 //if reached the end, finalize the process
552 if (me.completedFiles === me.totalFiles) {
555 key : 'download-progresss',
556 totalFiles : me.totalFiles,
557 completedFiles : me.completedFiles
560 me.completedFiles = me.totalFiles = me.percentage = me.downloadedFiles =
0;
561 delete me.percentages;
564 loadLocalFiles(callback);
574 When the download process is finished,
575 <code>MediaExplorer
</code> updates the media file list and the media player tree panel.
578 <p class=
"backtotop"><a href=
"#top">Back to top
</a></p>