| <meta name="doc-family" content="apps"> |
| <h1>Build Apps with Sencha Ext JS</h1> |
| |
| <p> |
| The goal of this doc is to get you started |
| on building packaged apps with the |
| <a href="http://www.sencha.com/products/extjs">Sencha Ext JS</a> framework. |
| To achieve this goal, |
| we will dive into a media player app built by Sencha. |
| The <a href="https://github.com/GoogleChrome/sencha-video-player-app">source code</a> |
| and <a href="http://senchaprosvcs.github.com/GooglePlayer/docs/output/#!/api">API Documentation</a> are available on GitHub. |
| </p> |
| |
| <p> |
| This app discovers a user's available media servers, |
| including media devices connected to the pc and |
| software that manages media over the network. |
| Users can browse media, play over the network, |
| or save offline. |
| </p> |
| |
| <p>Here are the key things you must do |
| to build a media player app using Sencha Ext JS: |
| </p> |
| |
| <ul> |
| <li>Create manifest, <code>manifest.json</code>.</li> |
| <li>Create <a href="app_lifecycle.html#eventpage">event page</a>, |
| <code>background.js</code>.</li> |
| <li><a href="app_external.html#sandboxing">Sandbox</a> app's logic.</li> |
| <li>Communicate between packaged app and sandboxed files.</li> |
| <li>Discover media servers.</li> |
| <li>Explore and play media.</li> |
| <li>Save media offline.</li> |
| </ul> |
| |
| <h2 id="first">Create manifest</h2> |
| |
| <p> |
| All packaged apps require a |
| <a href="manifest.html">manifest file</a> |
| which contains the information Chrome needs to launch apps. |
| As indicated in the manifest, |
| the media player app is "offline_enabled"; |
| media assets can be saved locally, |
| accessed and played regardless of connectivity. |
| </p> |
| |
| <p> |
| The "sandbox" field is used |
| to sandbox the app's main logic in a unique origin. |
| All sandboxed content is exempt from the packaged app |
| <a href="contentSecurityPolicy.html">Content Security Policy</a>, |
| but cannot directly access the packaged app APIs. |
| The manifest also includes the "socket" permission; |
| the media player app uses the <a href="socket.html">socket API</a> |
| to connect to a media server over the network. |
| </p> |
| |
| <pre> |
| { |
| "name": "Video Player", |
| "description": "Features network media discovery and playlist management", |
| "version": "1.0.0", |
| "manifest_version": 2, |
| "offline_enabled": true, |
| "app": { |
| "background": { |
| "scripts": [ |
| "background.js" |
| ] |
| } |
| }, |
| ... |
| |
| "sandbox": { |
| "pages": ["sandbox.html"] |
| }, |
| "permissions": [ |
| "experimental", |
| "http://*/*", |
| "unlimitedStorage", |
| { |
| "socket": [ |
| "tcp-connect", |
| "udp-send-to", |
| "udp-bind" |
| ] |
| } |
| ] |
| } |
| </pre> |
| |
| <h2 id="second">Create event page</h2> |
| |
| <p> |
| All packaged apps require <code>background.js</code> |
| to launch the application. |
| The media player's main page, <code>index.html</code>, |
| opens in a window with the specified dimensions: |
| </p> |
| |
| <pre> |
| chrome.app.runtime.onLaunched.addListener(function(launchData) { |
| var opt = { |
| width: 1000, |
| height: 700 |
| }; |
| |
| chrome.app.window.create('index.html', opt, function (win) { |
| win.launchData = launchData; |
| }); |
| |
| }); |
| </pre> |
| |
| <h2 id="three">Sandbox app's logic</h2> |
| |
| <p>Packaged apps run in a controlled environment |
| that enforces a strict <a href="contentSecurityPolicy.html">Content Security Policy (CSP)</a>. |
| The media player app needs some higher privileges to render the Ext JS components. |
| To comply with CSP and execute the app logic, |
| the app's main page, <code>index.html</code>, creates an iframe |
| that acts as a sandbox environment: |
| |
| <pre> |
| <iframe id="sandbox-frame" class="sandboxed" sandbox="allow-scripts" src="sandbox.html"></iframe> |
| </pre> |
| |
| <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: |
| </p> |
| |
| <pre> |
| <html> |
| <head> |
| <link rel="stylesheet" type="text/css" href="resources/css/app.css" />' |
| <script src="sdk/ext-all-dev.js"></script>' |
| <script src="lib/ext/data/PostMessage.js"></script>' |
| <script src="lib/ChromeProxy.js"></script>' |
| <script src="app.js"></script> |
| </head> |
| <body></body> |
| </html> |
| </pre> |
| |
| <p> |
| 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. |
| Since this script is sandboxed, it cannot directly access the packaged app APIs. |
| Communication between <code>app.js</code> and non-sandboxed files is done using the |
| <a href="https://developer.mozilla.org/en-US/docs/DOM/window.postMessage">HTML5 Post Message API</a>. |
| </p> |
| |
| <h2 id="four">Communicate between files</h2> |
| |
| <p> |
| In order for the media player app to access packaged app APIs, |
| like query the network for media servers, <code>app.js</code> posts messages |
| to <a href="https://github.com/GoogleChrome/sencha-video-player-app/blob/master/index.js">index.js</a>. |
| Unlike the sandboxed <code>app.js</code>, |
| <code>index.js</code> can directly access the packaged app APIs. |
| </p> |
| |
| <p> |
| <code>index.js</code> creates the iframe: |
| </p> |
| |
| <pre> |
| var iframe = document.getElementById('sandbox-frame'); |
| |
| iframeWindow = iframe.contentWindow; |
| </pre> |
| |
| <p> |
| And listens for messages from the sandboxed files: |
| </p> |
| |
| <pre> |
| window.addEventListener('message', function(e) { |
| var data= e.data, |
| key = data.key; |
| |
| console.log('[index.js] Post Message received with key ' + key); |
| |
| switch (key) { |
| case 'extension-baseurl': |
| extensionBaseUrl(data); |
| break; |
| |
| case 'upnp-discover': |
| upnpDiscover(data); |
| break; |
| |
| case 'upnp-browse': |
| upnpBrowse(data); |
| break; |
| |
| case 'play-media': |
| playMedia(data); |
| break; |
| |
| case 'download-media': |
| downloadMedia(data); |
| break; |
| |
| case 'cancel-download': |
| cancelDownload(data); |
| break; |
| |
| default: |
| console.log('[index.js] unidentified key for Post Message: "' + key + '"'); |
| } |
| }, false); |
| </pre> |
| |
| <p> |
| In the following example, |
| <code>app.js</code> sends a message to <code>index.js</code> |
| requesting the key 'extension-baseurl': |
| </p> |
| |
| <pre> |
| Ext.data.PostMessage.request({ |
| key: 'extension-baseurl', |
| success: function(data) { |
| //... |
| } |
| }); |
| </pre> |
| |
| <p> |
| <code>index.js</code> receives the request, assigns the result, |
| and replies by sending the Base URL back: |
| </p> |
| |
| <pre> |
| function extensionBaseUrl(data) { |
| data.result = chrome.extension.getURL('/'); |
| iframeWindow.postMessage(data, '*'); |
| } |
| </pre> |
| |
| <h2 id="five">Discover media servers</h2> |
| |
| <p> |
| There's a lot that goes into discovering media servers. |
| At a high level, the discovery workflow is initiated |
| by a user action to search for available media servers. |
| The <a href="https://github.com/GoogleChrome/sencha-video-player-app/blob/master/app/controller/MediaServers.js">MediaServer controller</a> |
| posts a message to <code>index.js</code>; |
| <code>index.js</code> listens for this message and when received, |
| calls <a href="https://github.com/GoogleChrome/sencha-video-player-app/blob/master/lib/Upnp.js">Upnp.js</a>. |
| </p> |
| |
| <p> |
| The <code>Upnp library</code> uses the packaged app |
| <a href="app_network.html">socket API</a> |
| to connect the media player app with any discovered media servers |
| and receive media data from the media server. |
| <code>Upnp.js</code> also uses |
| <a href="https://github.com/GoogleChrome/sencha-video-player-app/blob/master/lib/soapclient.js">soapclient.js</a> |
| to parse the media server data. |
| The remainder of this section describes this workflow in more detail. |
| </p> |
| |
| <h3 id="post">Post message</h3> |
| |
| <p> |
| When a user clicks the Media Servers button in the center of the media player app, |
| <code>MediaServers</code> calls <code>discoverServers()</code>. |
| This function first checks for any outstanding discovery requests, |
| and if true, aborts them so the new request can be initiated. |
| Next, the controller posts a message to <code>index.js</code> |
| with a key upnp-discovery, and two callback listeners: |
| </p> |
| |
| <pre> |
| me.activeDiscoverRequest = Ext.data.PostMessage.request({ |
| key: 'upnp-discover', |
| success: function(data) { |
| var items = []; |
| delete me.activeDiscoverRequest; |
| |
| if (serversGraph.isDestroyed) { |
| return; |
| } |
| |
| mainBtn.isLoading = false; |
| mainBtn.removeCls('pop-in'); |
| mainBtn.setIconCls('ico-server'); |
| mainBtn.setText('Media Servers'); |
| |
| //add servers |
| Ext.each(data, function(server) { |
| var icon, |
| urlBase = server.urlBase; |
| |
| if (urlBase) { |
| if (urlBase.substr(urlBase.length-1, 1) === '/'){ |
| urlBase = urlBase.substr(0, urlBase.length-1); |
| } |
| } |
| |
| if (server.icons && server.icons.length) { |
| if (server.icons[1]) { |
| icon = server.icons[1].url; |
| } |
| else { |
| icon = server.icons[0].url; |
| } |
| |
| icon = urlBase + icon; |
| } |
| |
| items.push({ |
| itemId: server.id, |
| text: server.friendlyName, |
| icon: icon, |
| data: server |
| }); |
| }); |
| |
| ... |
| }, |
| failure: function() { |
| delete me.activeDiscoverRequest; |
| |
| if (serversGraph.isDestroyed) { |
| return; |
| } |
| |
| mainBtn.isLoading = false; |
| mainBtn.removeCls('pop-in'); |
| mainBtn.setIconCls('ico-error'); |
| mainBtn.setText('Error...click to retry'); |
| } |
| }); |
| </pre> |
| |
| <h3 id="call">Call upnpDiscover()</h3> |
| |
| <p> |
| <code>index.js</code> listens |
| for the 'upnp-discover' message from <code>app.js</code> |
| and responds by calling <code>upnpDiscover()</code>. |
| When a media server is discovered, |
| <code>index.js</code> extracts the media server domain from the parameters, |
| saves the server locally, formats the media server data, |
| and pushes the data to the <code>MediaServer</code> controller. |
| </p> |
| |
| <h3 id="parse">Parse media server data</h3> |
| |
| <p> |
| When <code>Upnp.js</code> discovers a new media server, |
| it then retrieves a description of the device |
| and sends a Soaprequest to browse and parse the media server data; |
| <code>soapclient.js</code> parses the media elements by tag name |
| into a document. |
| </p> |
| |
| <h3 id="connect">Connect to media server</h3> |
| |
| <p> |
| <code>Upnp.js</code> connects to discovered media servers |
| and receives media data using the packaged app socket API: |
| </p> |
| |
| <pre> |
| socket.create("udp", {}, function(info) { |
| var socketId = info.socketId; |
| |
| //bind locally |
| socket.bind(socketId, "0.0.0.0", 0, function(info) { |
| |
| //pack upnp message |
| var message = String.toBuffer(UPNP_MESSAGE); |
| |
| //broadcast to upnp |
| socket.sendTo(socketId, message, UPNP_ADDRESS, UPNP_PORT, function(info) { |
| |
| // Wait 1 second |
| setTimeout(function() { |
| |
| //receive |
| socket.recvFrom(socketId, function(info) { |
| |
| //unpack message |
| var data = String.fromBuffer(info.data), |
| servers = [], |
| locationReg = /^location:/i; |
| |
| //extract location info |
| if (data) { |
| data = data.split("\r\n"); |
| |
| data.forEach(function(value) { |
| if (locationReg.test(value)){ |
| servers.push(value.replace(locationReg, "").trim()); |
| } |
| }); |
| } |
| |
| //success |
| callback(servers); |
| }); |
| |
| }, 1000); |
| }); |
| }); |
| }); |
| </pre> |
| |
| |
| <h2 id="six">Explore and play media</h2> |
| |
| <p> |
| The |
| <a href="https://github.com/GoogleChrome/sencha-video-player-app/blob/master/app/controller/MediaExplorer.js">MediaExplorer controller</a> |
| lists all the media files inside a media server folder |
| and is responsible for updating the breadcrumb navigation |
| in the media player app window. |
| When a user selects a media file, |
| the controller posts a message to <code>index.js</code> |
| with the 'play-media' key: |
| </p> |
| |
| <pre> |
| onFileDblClick: function(explorer, record) { |
| var serverPanel, node, |
| type = record.get('type'), |
| url = record.get('url'), |
| name = record.get('name'), |
| serverId= record.get('serverId'); |
| |
| if (type === 'audio' || type === 'video') { |
| Ext.data.PostMessage.request({ |
| key : 'play-media', |
| params : { |
| url: url, |
| name: name, |
| type: type |
| } |
| }); |
| } |
| }, |
| </pre> |
| |
| <p> |
| <code>index.js</code> listens for this post message and |
| responds by calling <code>playMedia()</code>: |
| </p> |
| |
| <pre> |
| function playMedia(data) { |
| var type = data.params.type, |
| url = data.params.url, |
| playerCt = document.getElementById('player-ct'), |
| audioBody = document.getElementById('audio-body'), |
| videoBody = document.getElementById('video-body'), |
| mediaEl = playerCt.getElementsByTagName(type)[0], |
| mediaBody = type === 'video' ? videoBody : audioBody, |
| isLocal = false; |
| |
| //save data |
| filePlaying = { |
| url : url, |
| type: type, |
| name: data.params.name |
| }; |
| |
| //hide body els |
| audioBody.style.display = 'none'; |
| videoBody.style.display = 'none'; |
| |
| var animEnd = function(e) { |
| |
| //show body el |
| mediaBody.style.display = ''; |
| |
| //play media |
| mediaEl.play(); |
| |
| //clear listeners |
| playerCt.removeEventListener( 'webkitTransitionEnd', animEnd, false ); |
| animEnd = null; |
| }; |
| |
| //load media |
| mediaEl.src = url; |
| mediaEl.load(); |
| |
| //animate in player |
| playerCt.addEventListener( 'webkitTransitionEnd', animEnd, false ); |
| playerCt.style.webkitTransform = "translateY(0)"; |
| |
| //reply postmessage |
| data.result = true; |
| sendMessage(data); |
| } |
| </pre> |
| |
| <h2 id="seven">Save media offline</h2> |
| |
| <p> |
| Most of the hard work to save media offline is done by the |
| <a href="https://github.com/GoogleChrome/sencha-video-player-app/blob/master/lib/filer.js">filer.js library</a>. |
| You can read more this library in |
| <a href="http://ericbidelman.tumblr.com/post/14866798359/introducing-filer-js">Introducing filer.js</a>. |
| </p> |
| |
| <p> |
| The process kicks off when a user selects one or more files |
| and initiates the 'Take offline' action. |
| The |
| <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> |
| with a key 'download-media'; <code>index.js</code> listens for this message |
| and calls the <code>downloadMedia()</code> function |
| to initiate the download process: |
| </p> |
| |
| <pre> |
| function downloadMedia(data) { |
| DownloadProcess.run(data.params.files, function() { |
| data.result = true; |
| sendMessage(data); |
| }); |
| } |
| </pre> |
| |
| <p> |
| The <code>DownloadProcess</code> utility method creates an xhr request |
| to get data from the media server and waits for completion status. |
| This initiates the onload callback which checks the received content |
| and saves the data locally using the <code>filer.js</code> function: |
| </p> |
| |
| <pre> |
| filer.write( |
| saveUrl, |
| { |
| data: Util.arrayBufferToBlob(fileArrayBuf), |
| type: contentType |
| }, |
| function(fileEntry, fileWriter) { |
| |
| console.log('file saved!'); |
| |
| //increment downloaded |
| me.completedFiles++; |
| |
| //if reached the end, finalize the process |
| if (me.completedFiles === me.totalFiles) { |
| |
| sendMessage({ |
| key : 'download-progresss', |
| totalFiles : me.totalFiles, |
| completedFiles : me.completedFiles |
| }); |
| |
| me.completedFiles = me.totalFiles = me.percentage = me.downloadedFiles = 0; |
| delete me.percentages; |
| |
| //reload local |
| loadLocalFiles(callback); |
| } |
| }, |
| function(e) { |
| console.log(e); |
| } |
| ); |
| </pre> |
| |
| <p> |
| When the download process is finished, |
| <code>MediaExplorer</code> updates the media file list and the media player tree panel. |
| </p> |
| |
| <p class="backtotop"><a href="#top">Back to top</a></p> |