1<meta name="doc-family" content="apps">
2<h1>Build Apps with Sencha Ext JS</h1>
3
4<p>
5The goal of this doc is to get you started
6on building Chrome Apps with the
7<a href="http://www.sencha.com/products/extjs">Sencha Ext JS</a> framework.
8To achieve this goal,
9we will dive into a media player app built by Sencha.
10The <a href="https://github.com/GoogleChrome/sencha-video-player-app">source code</a>
11and <a href="http://senchaprosvcs.github.com/GooglePlayer/docs/output/#!/api">API Documentation</a> are available on GitHub.
12</p>
13
14<p>
15This app discovers a user's available media servers,
16including media devices connected to the pc and
17software that manages media over the network. 
18Users can browse media, play over the network,
19or save offline.
20</p>
21
22<p>Here are the key things you must do
23to build a media player app using Sencha Ext JS:
24</p>
25
26<ul>
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>
35</ul>
36
37<h2 id="first">Create manifest</h2>
38
39<p>
40All Chrome Apps require a
41<a href="manifest">manifest file</a>
42which contains the information Chrome needs to launch apps.
43As indicated in the manifest,
44the media player app is "offline_enabled";
45media assets can be saved locally,
46accessed and played regardless of connectivity.
47</p>
48
49<p>
50The "sandbox" field is used
51to sandbox the app's main logic in a unique origin.
52All sandboxed content is exempt from the Chrome App
53<a href="contentSecurityPolicy">Content Security Policy</a>,
54but cannot directly access the Chrome App APIs. 
55The manifest also includes the "socket" permission;
56the media player app uses the <a href="socket">socket API</a>
57to connect to a media server over the network.
58</p>
59
60<pre data-filename="manifest.json">
61{
62    "name": "Video Player",
63    "description": "Features network media discovery and playlist management",
64    "version": "1.0.0",
65    "manifest_version": 2,
66    "offline_enabled": true,
67    "app": {
68        "background": {
69            "scripts": [
70                "background.js"
71            ]
72        }
73    },
74    ...
75
76    "sandbox": {
77        "pages": ["sandbox.html"]
78    },
79    "permissions": [
80        "experimental",
81        "http://*/*",
82        "unlimitedStorage",
83        {
84            "socket": [
85                "tcp-connect",
86                "udp-send-to",
87                "udp-bind"
88            ]
89        }
90    ]
91}
92</pre>
93
94<h2 id="second">Create event page</h2>
95
96<p>
97All Chrome Apps require <code>background.js</code>
98to launch the application.
99The media player's main page, <code>index.html</code>,
100opens in a window with the specified dimensions:
101</p>
102
103<pre data-filename="background.js">
104chrome.app.runtime.onLaunched.addListener(function(launchData) {
105    var opt = {
106        width: 1000,
107        height: 700
108    };
109
110    chrome.app.window.create('index.html', opt, function (win) {
111        win.launchData = launchData;
112    });
113
114});
115</pre>
116
117<h2 id="three">Sandbox app's logic</h2>
118
119<p>Chrome Apps run in a controlled environment
120that enforces a strict <a href="contentSecurityPolicy">Content Security Policy (CSP)</a>.
121The media player app needs some higher privileges to render the Ext JS components.
122To comply with CSP and execute the app logic,
123the app's main page, <code>index.html</code>, creates an iframe
124that acts as a sandbox environment:
125
126<pre data-filename="index.html">
127&lt;iframe id="sandbox-frame" sandbox="allow-scripts" src="sandbox.html">&lt;/iframe>
128</pre>
129
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:
131</p>
132
133<pre data-filename="sandbox.html">
134&lt;html>
135&lt;head>
136    &lt;link rel="stylesheet" type="text/css" href="resources/css/app.css" />'
137    &lt;script src="sdk/ext-all-dev.js">&lt;/script>'
138    &lt;script src="lib/ext/data/PostMessage.js">&lt;/script>'
139    &lt;script src="lib/ChromeProxy.js">&lt;/script>'
140    &lt;script src="app.js">&lt;/script>
141&lt;/head>
142&lt;body>&lt;/body>
143&lt;/html>
144</pre>
145
146<p>
147The <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.
148Since this script is sandboxed, it cannot directly access the Chrome App APIs.
149Communication 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>.
151</p>
152
153<h2 id="four">Communicate between files</h2>
154
155<p>
156In order for the media player app to access Chrome App APIs,
157like query the network for media servers, <code>app.js</code> posts messages
158to <a href="https://github.com/GoogleChrome/sencha-video-player-app/blob/master/index.js">index.js</a>.
159Unlike the sandboxed <code>app.js</code>,
160<code>index.js</code> can directly access the Chrome App APIs.
161</p>
162
163<p>
164<code>index.js</code> creates the iframe:
165</p>
166
167<pre data-filename="index.js">
168var iframe = document.getElementById('sandbox-frame');
169
170iframeWindow = iframe.contentWindow;
171</pre>
172
173<p>
174And listens for messages from the sandboxed files:
175</p>
176
177<pre data-filename="index.js">
178window.addEventListener('message', function(e) {
179    var data= e.data,
180        key = data.key;
181
182    console.log('[index.js] Post Message received with key ' + key);
183
184    switch (key) {
185        case 'extension-baseurl':
186            extensionBaseUrl(data);
187            break;
188
189        case 'upnp-discover':
190            upnpDiscover(data);
191            break;
192
193        case 'upnp-browse':
194            upnpBrowse(data);
195            break;
196
197        case 'play-media':
198            playMedia(data);
199            break;
200
201        case 'download-media':
202            downloadMedia(data);
203            break;
204
205        case 'cancel-download':
206            cancelDownload(data);
207            break;
208
209        default:
210            console.log('[index.js] unidentified key for Post Message: "' + key + '"');
211    }
212}, false);
213</pre>
214
215<p>
216In the following example,
217<code>app.js</code> sends a message to <code>index.js</code>
218requesting the key 'extension-baseurl':
219</p>
220
221<pre data-filename="app.js">
222Ext.data.PostMessage.request({
223    key: 'extension-baseurl',
224    success: function(data) {
225        //...
226    }
227});
228</pre>
229
230<p>
231<code>index.js</code> receives the request, assigns the result,
232and replies by sending the Base URL back:
233</p>
234
235<pre data-filename="index.js">
236function extensionBaseUrl(data) {
237    data.result = chrome.extension.getURL('/');
238    iframeWindow.postMessage(data, '*');
239}
240</pre>
241
242<h2 id="five">Discover media servers</h2>
243
244<p>
245There's a lot that goes into discovering media servers.
246At a high level, the discovery workflow is initiated
247by a user action to search for available media servers.
248The <a href="https://github.com/GoogleChrome/sencha-video-player-app/blob/master/app/controller/MediaServers.js">MediaServer controller</a>
249posts a message to <code>index.js</code>;
250<code>index.js</code> listens for this message and when received,
251calls <a href="https://github.com/GoogleChrome/sencha-video-player-app/blob/master/lib/Upnp.js">Upnp.js</a>.
252</p>
253
254<p>
255The <code>Upnp library</code> uses the Chrome App
256<a href="app_network">socket API</a>
257to connect the media player app with any discovered media servers
258and 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>
261to parse the media server data.
262The remainder of this section describes this workflow in more detail.
263</p>
264
265<h3 id="post">Post message</h3>
266
267<p>
268When a user clicks the Media Servers button in the center of the media player app,
269<code>MediaServers.js</code> calls <code>discoverServers()</code>.
270This function first checks for any outstanding discovery requests,
271and if true, aborts them so the new request can be initiated.
272Next, the controller posts a message to <code>index.js</code>
273with a key upnp-discovery, and two callback listeners:
274</p>
275
276<pre data-filename="MediaServers.js">
277me.activeDiscoverRequest = Ext.data.PostMessage.request({
278    key: 'upnp-discover',
279    success: function(data) {
280        var items = [];
281        delete me.activeDiscoverRequest;
282
283        if (serversGraph.isDestroyed) {
284            return;
285        }
286
287        mainBtn.isLoading = false;
288        mainBtn.removeCls('pop-in');
289        mainBtn.setIconCls('ico-server');
290        mainBtn.setText('Media Servers');
291
292        //add servers
293        Ext.each(data, function(server) {
294            var icon,
295                urlBase = server.urlBase;
296
297            if (urlBase) {
298                if (urlBase.substr(urlBase.length-1, 1) === '/'){
299                        urlBase = urlBase.substr(0, urlBase.length-1);
300                }
301            }
302
303            if (server.icons && server.icons.length) {
304                if (server.icons[1]) {
305                    icon = server.icons[1].url;
306                }
307                else {
308                    icon = server.icons[0].url;
309                }
310
311                icon = urlBase + icon;
312            }
313
314            items.push({
315                itemId: server.id,
316                text: server.friendlyName,
317                icon: icon,
318                data: server
319            });
320        });
321
322        ...
323    },
324    failure: function() {
325        delete me.activeDiscoverRequest;
326                
327        if (serversGraph.isDestroyed) {
328            return;
329        }
330                
331        mainBtn.isLoading = false;
332        mainBtn.removeCls('pop-in');
333        mainBtn.setIconCls('ico-error');
334        mainBtn.setText('Error...click to retry');
335    }
336});
337</pre>
338
339<h3 id="call">Call upnpDiscover()</h3>
340
341<p>
342<code>index.js</code> listens
343for the 'upnp-discover' message from <code>app.js</code>
344and responds by calling <code>upnpDiscover()</code>.
345When a media server is discovered,
346<code>index.js</code> extracts the media server domain from the parameters,
347saves the server locally, formats the media server data,
348and pushes the data to the <code>MediaServer</code> controller.
349</p>
350
351<h3 id="parse">Parse media server data</h3>
352
353<p>
354When <code>Upnp.js</code> discovers a new media server,
355it then retrieves a description of the device
356and sends a Soaprequest to browse and parse the media server data;
357<code>soapclient.js</code> parses the media elements by tag name
358into a document.
359</p>
360
361<h3 id="connect">Connect to media server</h3>
362
363<p>
364<code>Upnp.js</code> connects to discovered media servers
365and receives media data using the Chrome App socket API:
366</p>
367
368<pre data-filename="Upnp.js">
369socket.create("udp", {}, function(info) {
370    var socketId = info.socketId;
371        
372    //bind locally
373    socket.bind(socketId, "0.0.0.0", 0, function(info) {
374            
375        //pack upnp message
376        var message = String.toBuffer(UPNP_MESSAGE);
377            
378        //broadcast to upnp
379        socket.sendTo(socketId, message, UPNP_ADDRESS, UPNP_PORT, function(info) {
380                
381            // Wait 1 second
382            setTimeout(function() {
383                    
384                //receive
385                socket.recvFrom(socketId, function(info) {
386
387                    //unpack message
388                    var data        = String.fromBuffer(info.data),
389                        servers     = [],
390                        locationReg = /^location:/i;
391                            
392                    //extract location info
393                    if (data) {
394                        data = data.split("\r\n");
395                            
396                        data.forEach(function(value) {
397                            if (locationReg.test(value)){
398                                servers.push(value.replace(locationReg, "").trim());
399                            }
400                        });
401                    }
402                            
403                    //success
404                    callback(servers);
405                });
406                    
407            }, 1000);
408        });
409    });
410});
411</pre>
412
413
414<h2 id="six">Explore and play media</h2>
415
416<p>
417The
418<a href="https://github.com/GoogleChrome/sencha-video-player-app/blob/master/app/controller/MediaExplorer.js">MediaExplorer controller</a>
419lists all the media files inside a media server folder
420and is responsible for updating the breadcrumb navigation
421in the media player app window.
422When a user selects a media file,
423the controller posts a message to <code>index.js</code>
424with the 'play-media' key:
425</p>
426
427<pre data-filename="MediaExplorer.js">
428onFileDblClick: 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');
434            
435    if (type === 'audio' || type === 'video') {
436        Ext.data.PostMessage.request({
437            key     : 'play-media',
438            params  : {
439                url: url,
440                name: name,
441                type: type
442            }
443        });
444    }
445},
446</pre>
447
448<p>
449<code>index.js</code> listens for this post message and
450responds by calling <code>playMedia()</code>:
451</p>
452
453<pre data-filename="index.js">
454function 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,
462        isLocal     = false;
463
464    //save data
465    filePlaying = {
466        url : url,
467        type: type,
468        name: data.params.name
469    };
470
471    //hide body els
472    audioBody.style.display = 'none';
473    videoBody.style.display = 'none';
474
475    var animEnd = function(e) {
476
477        //show body el
478        mediaBody.style.display = '';
479
480        //play media
481        mediaEl.play();
482
483        //clear listeners
484        playerCt.removeEventListener( 'webkitTransitionEnd', animEnd, false );
485        animEnd = null;
486    };
487
488    //load media
489    mediaEl.src = url;
490    mediaEl.load();
491
492    //animate in player
493    playerCt.addEventListener( 'webkitTransitionEnd', animEnd, false );
494    playerCt.style.webkitTransform = "translateY(0)";
495
496    //reply postmessage
497    data.result = true;
498    sendMessage(data);
499}
500</pre>
501
502<h2 id="seven">Save media offline</h2>
503
504<p>
505Most 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>.
507You can read more this library in
508<a href="http://ericbidelman.tumblr.com/post/14866798359/introducing-filer-js">Introducing filer.js</a>.
509</p>
510
511<p>
512The process kicks off when a user selects one or more files
513and initiates the 'Take offline' action.
514The
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>
516with a key 'download-media'; <code>index.js</code> listens for this message
517and calls the <code>downloadMedia()</code> function
518to initiate the download process:
519</p>
520
521<pre data-filename="index.js">
522function downloadMedia(data) {
523        DownloadProcess.run(data.params.files, function() {
524            data.result = true;
525            sendMessage(data);
526        });
527    }
528</pre>
529
530<p>
531The <code>DownloadProcess</code> utility method creates an xhr request
532to get data from the media server and waits for completion status.
533This initiates the onload callback which checks the received content
534and saves the data locally using the <code>filer.js</code> function:
535</p>
536
537<pre data-filename="filer.js">
538filer.write(
539    saveUrl,
540    {
541        data: Util.arrayBufferToBlob(fileArrayBuf),
542        type: contentType
543    },
544    function(fileEntry, fileWriter) {
545
546        console.log('file saved!');
547
548        //increment downloaded
549        me.completedFiles++;
550
551        //if reached the end, finalize the process
552        if (me.completedFiles === me.totalFiles) {
553
554            sendMessage({
555                key             : 'download-progresss',
556                totalFiles      : me.totalFiles,
557                completedFiles  : me.completedFiles
558            });
559
560            me.completedFiles = me.totalFiles = me.percentage = me.downloadedFiles = 0;
561            delete me.percentages;
562
563            //reload local
564            loadLocalFiles(callback);
565        }
566    },
567    function(e) {
568        console.log(e);
569    }
570);
571</pre>
572
573<p>
574When the download process is finished,
575<code>MediaExplorer</code> updates the media file list and the media player tree panel.
576</p>
577
578<p class="backtotop"><a href="#top">Back to top</a></p>
579