| 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..a56f24dbc2b3cdfd4c35a2e12f60671e4ee29ace
|
| --- /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 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="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 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>
|
|
|