| OLD | NEW |
| (Empty) | |
| 1 <meta name="doc-family" content="apps"> |
| 2 <h1>Build Apps with Sencha Ext JS</h1> |
| 3 |
| 4 <p> |
| 5 The goal of this doc is to get you started |
| 6 on building packaged apps with the |
| 7 <a href="http://www.sencha.com/products/extjs">Sencha Ext JS</a> framework. |
| 8 To achieve this goal, |
| 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 cod
e</a> |
| 11 and <a href="http://senchaprosvcs.github.com/GooglePlayer/docs/output/#!/api">AP
I Documentation</a> are available on GitHub. |
| 12 </p> |
| 13 |
| 14 <p> |
| 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, |
| 19 or save offline. |
| 20 </p> |
| 21 |
| 22 <p>Here are the key things you must do |
| 23 to 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.html#eventpage">event page</a>, |
| 29 <code>background.js</code>.</li> |
| 30 <li><a href="app_external.html#sandboxing">Sandbox</a> app's logic.</li> |
| 31 <li>Communicate between packaged 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> |
| 40 All packaged apps require a |
| 41 <a href="manifest.html">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. |
| 47 </p> |
| 48 |
| 49 <p> |
| 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 packaged app |
| 53 <a href="app_csp.html">Content Security Policy</a>, |
| 54 but cannot directly access the packaged app APIs. |
| 55 The manifest also includes the "socket" permission; |
| 56 the media player app uses the <a href="socket.html">socket API</a> |
| 57 to connect to a media server over the network. |
| 58 </p> |
| 59 |
| 60 <pre> |
| 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> |
| 97 All packaged 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: |
| 101 </p> |
| 102 |
| 103 <pre> |
| 104 chrome.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>Packaged apps run in a controlled environment |
| 120 that enforces a strict <a href="app_csp.html">Content Security Policy (CSP)</a>. |
| 121 The media player app needs some higher privileges to render the Ext JS component
s. |
| 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: |
| 125 |
| 126 <pre> |
| 127 <iframe id="sandbox-frame" class="sandboxed" sandbox="allow-scripts" src="san
dbox.html"></iframe> |
| 128 </pre> |
| 129 |
| 130 <p>The iframe points to <a href="https://github.com/GoogleChrome/sencha-video-pl
ayer-app/blob/master/sandbox.html">sandbox.html</a> which includes the files req
uired for the Ext JS application: |
| 131 </p> |
| 132 |
| 133 <pre> |
| 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> |
| 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 packaged 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>. |
| 151 </p> |
| 152 |
| 153 <h2 id="four">Communicate between files</h2> |
| 154 |
| 155 <p> |
| 156 In order for the media player app to access packaged 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 packaged app APIs. |
| 161 </p> |
| 162 |
| 163 <p> |
| 164 <code>index.js</code> creates the iframe: |
| 165 </p> |
| 166 |
| 167 <pre> |
| 168 var iframe = document.getElementById('sandbox-frame'); |
| 169 |
| 170 iframeWindow = iframe.contentWindow; |
| 171 </pre> |
| 172 |
| 173 <p> |
| 174 And listens for messages from the sandboxed files: |
| 175 </p> |
| 176 |
| 177 <pre> |
| 178 window.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> |
| 216 In the following example, |
| 217 <code>app.js</code> sends a message to <code>index.js</code> |
| 218 requesting the key 'extension-baseurl': |
| 219 </p> |
| 220 |
| 221 <pre> |
| 222 Ext.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, |
| 232 and replies by sending the Base URL back: |
| 233 </p> |
| 234 |
| 235 <pre> |
| 236 function 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> |
| 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/mast
er/lib/Upnp.js">Upnp.js</a>. |
| 252 </p> |
| 253 |
| 254 <p> |
| 255 The <code>Upnp library</code> uses the packaged app |
| 256 <a href="app_network.html">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. |
| 263 </p> |
| 264 |
| 265 <h3 id="post">Post message</h3> |
| 266 |
| 267 <p> |
| 268 When a user clicks the Media Servers button in the center of the media player ap
p, |
| 269 <code>MediaServers</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: |
| 274 </p> |
| 275 |
| 276 <pre> |
| 277 me.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 |
| 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. |
| 349 </p> |
| 350 |
| 351 <h3 id="parse">Parse media server data</h3> |
| 352 |
| 353 <p> |
| 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 |
| 358 into 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 |
| 365 and receives media data using the packaged app socket API: |
| 366 </p> |
| 367 |
| 368 <pre> |
| 369 socket.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> |
| 417 The |
| 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: |
| 425 </p> |
| 426 |
| 427 <pre> |
| 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'); |
| 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 |
| 450 responds by calling <code>playMedia()</code>: |
| 451 </p> |
| 452 |
| 453 <pre> |
| 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, |
| 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> |
| 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">I
ntroducing filer.js</a>. |
| 509 </p> |
| 510 |
| 511 <p> |
| 512 The process kicks off when a user selects one or more files |
| 513 and initiates the 'Take offline' action. |
| 514 The |
| 515 <a href="https://github.com/GoogleChrome/sencha-video-player-app/blob/master/app
/controller/MediaExplorer.js">MediaExplorer controller</a> posts a message to <c
ode>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: |
| 519 </p> |
| 520 |
| 521 <pre> |
| 522 function downloadMedia(data) { |
| 523 DownloadProcess.run(data.params.files, function() { |
| 524 data.result = true; |
| 525 sendMessage(data); |
| 526 }); |
| 527 } |
| 528 </pre> |
| 529 |
| 530 <p> |
| 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: |
| 535 </p> |
| 536 |
| 537 <pre> |
| 538 filer.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.downloadedFil
es = 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> |
| 574 When 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> |
| OLD | NEW |