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<iframe id="sandbox-frame" sandbox="allow-scripts" src="sandbox.html"></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<html> 135<head> 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> 141</head> 142<body></body> 143</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