Chromium Code Reviews| Index: chrome/common/extensions/docs/templates/articles/sencha_framework.html |
| diff --git a/chrome/common/extensions/docs/templates/articles/sencha_framework.html b/chrome/common/extensions/docs/templates/articles/sencha_framework.html |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..48adf6e9010cbafa25488e785357debf4412bf5b |
| --- /dev/null |
| +++ b/chrome/common/extensions/docs/templates/articles/sencha_framework.html |
| @@ -0,0 +1,578 @@ |
| +<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 build 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 localled, |
| +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="app_csp.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="app_csp.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>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>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>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>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="four">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="five">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 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.
|
| +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> |