Chromium Code Reviews| 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 build 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 localled, | |
| 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>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>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>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>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="four">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="five">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 intiates the onload callback which checks the received content | |
|
PaulKinlan
2012/10/04 08:18:51
Spelling intiates?
mkearney1
2012/10/09 22:40:43
Done.
| |
| 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 |