Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(215)

Side by Side Diff: chrome/common/extensions/docs/templates/articles/sencha_framework.html

Issue 10985050: New getting started tutorial for sencha media player app. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Created 8 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | chrome/common/extensions/docs/templates/private/apps_sidenav.html » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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 built 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 locally,
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 &lt;iframe id="sandbox-frame" class="sandboxed" sandbox="allow-scripts" src="san dbox.html">&lt;/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 &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 <script src="app.js"></script>
141 &lt;/head>
142 &lt;body></body>
143 &lt;/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 id="post">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 id="call">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 id="parse">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 id="connect">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="six">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="seven">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 initiates the onload callback which checks the received content
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>
OLDNEW
« no previous file with comments | « no previous file | chrome/common/extensions/docs/templates/private/apps_sidenav.html » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698