Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / common / extensions / docs / templates / articles / sencha_framework.html
blob3045560d5f250fa8dd9171d36234649f561ea568
1 <meta name="doc-family" content="apps">
2 <h1>Build Apps with Sencha Ext JS</h1>
4 <p>
5 The goal of this doc is to get you started
6 on building Chrome 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 code</a>
11 and <a href="http://senchaprosvcs.github.com/GooglePlayer/docs/output/#!/api">API Documentation</a> are available on GitHub.
12 </p>
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>
22 <p>Here are the key things you must do
23 to build a media player app using Sencha Ext JS:
24 </p>
26 <ul>
27 <li>Create manifest, <code>manifest.json</code>.</li>
28 <li>Create <a href="app_lifecycle#eventpage">event page</a>,
29 <code>background.js</code>.</li>
30 <li><a href="app_external#sandboxing">Sandbox</a> app's logic.</li>
31 <li>Communicate between Chrome 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>
37 <h2 id="first">Create manifest</h2>
39 <p>
40 All Chrome Apps require a
41 <a href="manifest">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>
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 Chrome App
53 <a href="contentSecurityPolicy">Content Security Policy</a>,
54 but cannot directly access the Chrome App APIs.
55 The manifest also includes the "socket" permission;
56 the media player app uses the <a href="socket">socket API</a>
57 to connect to a media server over the network.
58 </p>
60 <pre data-filename="manifest.json">
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"
74 ...
76 "sandbox": {
77 "pages": ["sandbox.html"]
79 "permissions": [
80 "experimental",
81 "http://*/*",
82 "unlimitedStorage",
84 "socket": [
85 "tcp-connect",
86 "udp-send-to",
87 "udp-bind"
92 </pre>
94 <h2 id="second">Create event page</h2>
96 <p>
97 All Chrome 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>
103 <pre data-filename="background.js">
104 chrome.app.runtime.onLaunched.addListener(function(launchData) {
105 var opt = {
106 width: 1000,
107 height: 700
110 chrome.app.window.create('index.html', opt, function (win) {
111 win.launchData = launchData;
115 </pre>
117 <h2 id="three">Sandbox app's logic</h2>
119 <p>Chrome Apps run in a controlled environment
120 that enforces a strict <a href="contentSecurityPolicy">Content Security Policy (CSP)</a>.
121 The media player app needs some higher privileges to render the Ext JS components.
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:
126 <pre data-filename="index.html">
127 &lt;iframe id="sandbox-frame" sandbox="allow-scripts" src="sandbox.html">&lt;/iframe>
128 </pre>
130 <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:
131 </p>
133 <pre data-filename="sandbox.html">
134 &lt;html>
135 &lt;head>
136 &lt;link rel="stylesheet" type="text/css" href="resources/css/app.css" />'
137 &lt;script src="sdk/ext-all-dev.js">&lt;/script>'
138 &lt;script src="lib/ext/data/PostMessage.js">&lt;/script>'
139 &lt;script src="lib/ChromeProxy.js">&lt;/script>'
140 &lt;script src="app.js">&lt;/script>
141 &lt;/head>
142 &lt;body>&lt;/body>
143 &lt;/html>
144 </pre>
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 Chrome 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>
153 <h2 id="four">Communicate between files</h2>
156 In order for the media player app to access Chrome 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 Chrome App APIs.
161 </p>
164 <code>index.js</code> creates the iframe:
165 </p>
167 <pre data-filename="index.js">
168 var iframe = document.getElementById('sandbox-frame');
170 iframeWindow = iframe.contentWindow;
171 </pre>
174 And listens for messages from the sandboxed files:
175 </p>
177 <pre data-filename="index.js">
178 window.addEventListener('message', function(e) {
179 var data= e.data,
180 key = data.key;
182 console.log('[index.js] Post Message received with key ' + key);
184 switch (key) {
185 case 'extension-baseurl':
186 extensionBaseUrl(data);
187 break;
189 case 'upnp-discover':
190 upnpDiscover(data);
191 break;
193 case 'upnp-browse':
194 upnpBrowse(data);
195 break;
197 case 'play-media':
198 playMedia(data);
199 break;
201 case 'download-media':
202 downloadMedia(data);
203 break;
205 case 'cancel-download':
206 cancelDownload(data);
207 break;
209 default:
210 console.log('[index.js] unidentified key for Post Message: "' + key + '"');
212 }, false);
213 </pre>
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>
221 <pre data-filename="app.js">
222 Ext.data.PostMessage.request({
223 key: 'extension-baseurl',
224 success: function(data) {
225 //...
228 </pre>
231 <code>index.js</code> receives the request, assigns the result,
232 and replies by sending the Base URL back:
233 </p>
235 <pre data-filename="index.js">
236 function extensionBaseUrl(data) {
237 data.result = chrome.extension.getURL('/');
238 iframeWindow.postMessage(data, '*');
240 </pre>
242 <h2 id="five">Discover media servers</h2>
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/master/lib/Upnp.js">Upnp.js</a>.
252 </p>
255 The <code>Upnp library</code> uses the Chrome App
256 <a href="app_network">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>
265 <h3 id="post">Post message</h3>
268 When a user clicks the Media Servers button in the center of the media player app,
269 <code>MediaServers.js</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>
276 <pre data-filename="MediaServers.js">
277 me.activeDiscoverRequest = Ext.data.PostMessage.request({
278 key: 'upnp-discover',
279 success: function(data) {
280 var items = [];
281 delete me.activeDiscoverRequest;
283 if (serversGraph.isDestroyed) {
284 return;
287 mainBtn.isLoading = false;
288 mainBtn.removeCls('pop-in');
289 mainBtn.setIconCls('ico-server');
290 mainBtn.setText('Media Servers');
292 //add servers
293 Ext.each(data, function(server) {
294 var icon,
295 urlBase = server.urlBase;
297 if (urlBase) {
298 if (urlBase.substr(urlBase.length-1, 1) === '/'){
299 urlBase = urlBase.substr(0, urlBase.length-1);
303 if (server.icons && server.icons.length) {
304 if (server.icons[1]) {
305 icon = server.icons[1].url;
307 else {
308 icon = server.icons[0].url;
311 icon = urlBase + icon;
314 items.push({
315 itemId: server.id,
316 text: server.friendlyName,
317 icon: icon,
318 data: server
324 failure: function() {
325 delete me.activeDiscoverRequest;
327 if (serversGraph.isDestroyed) {
328 return;
331 mainBtn.isLoading = false;
332 mainBtn.removeCls('pop-in');
333 mainBtn.setIconCls('ico-error');
334 mainBtn.setText('Error...click to retry');
337 </pre>
339 <h3 id="call">Call upnpDiscover()</h3>
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>
351 <h3 id="parse">Parse media server data</h3>
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>
361 <h3 id="connect">Connect to media server</h3>
364 <code>Upnp.js</code> connects to discovered media servers
365 and receives media data using the Chrome App socket API:
366 </p>
368 <pre data-filename="Upnp.js">
369 socket.create("udp", {}, function(info) {
370 var socketId = info.socketId;
372 //bind locally
373 socket.bind(socketId, "0.0.0.0", 0, function(info) {
375 //pack upnp message
376 var message = String.toBuffer(UPNP_MESSAGE);
378 //broadcast to upnp
379 socket.sendTo(socketId, message, UPNP_ADDRESS, UPNP_PORT, function(info) {
381 // Wait 1 second
382 setTimeout(function() {
384 //receive
385 socket.recvFrom(socketId, function(info) {
387 //unpack message
388 var data = String.fromBuffer(info.data),
389 servers = [],
390 locationReg = /^location:/i;
392 //extract location info
393 if (data) {
394 data = data.split("\r\n");
396 data.forEach(function(value) {
397 if (locationReg.test(value)){
398 servers.push(value.replace(locationReg, "").trim());
403 //success
404 callback(servers);
407 }, 1000);
411 </pre>
414 <h2 id="six">Explore and play media</h2>
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>
427 <pre data-filename="MediaExplorer.js">
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');
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
446 </pre>
449 <code>index.js</code> listens for this post message and
450 responds by calling <code>playMedia()</code>:
451 </p>
453 <pre data-filename="index.js">
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;
464 //save data
465 filePlaying = {
466 url : url,
467 type: type,
468 name: data.params.name
471 //hide body els
472 audioBody.style.display = 'none';
473 videoBody.style.display = 'none';
475 var animEnd = function(e) {
477 //show body el
478 mediaBody.style.display = '';
480 //play media
481 mediaEl.play();
483 //clear listeners
484 playerCt.removeEventListener( 'webkitTransitionEnd', animEnd, false );
485 animEnd = null;
488 //load media
489 mediaEl.src = url;
490 mediaEl.load();
492 //animate in player
493 playerCt.addEventListener( 'webkitTransitionEnd', animEnd, false );
494 playerCt.style.webkitTransform = "translateY(0)";
496 //reply postmessage
497 data.result = true;
498 sendMessage(data);
500 </pre>
502 <h2 id="seven">Save media offline</h2>
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">Introducing filer.js</a>.
509 </p>
512 The process kicks off when a user selects one or more files
513 and initiates the 'Take offline' action.
515 <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>
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>
521 <pre data-filename="index.js">
522 function downloadMedia(data) {
523 DownloadProcess.run(data.params.files, function() {
524 data.result = true;
525 sendMessage(data);
528 </pre>
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>
537 <pre data-filename="filer.js">
538 filer.write(
539 saveUrl,
541 data: Util.arrayBufferToBlob(fileArrayBuf),
542 type: contentType
544 function(fileEntry, fileWriter) {
546 console.log('file saved!');
548 //increment downloaded
549 me.completedFiles++;
551 //if reached the end, finalize the process
552 if (me.completedFiles === me.totalFiles) {
554 sendMessage({
555 key : 'download-progresss',
556 totalFiles : me.totalFiles,
557 completedFiles : me.completedFiles
560 me.completedFiles = me.totalFiles = me.percentage = me.downloadedFiles = 0;
561 delete me.percentages;
563 //reload local
564 loadLocalFiles(callback);
567 function(e) {
568 console.log(e);
571 </pre>
574 When the download process is finished,
575 <code>MediaExplorer</code> updates the media file list and the media player tree panel.
576 </p>
578 <p class="backtotop"><a href="#top">Back to top</a></p>