用 Sencha Ext JS 建立应用
该文档的目的是使您初步了解如何使用 Sencha Ext JS 框架建立 Chrome 应用。要实现这一目的,我们将会深入讨论一个通过 Sencha 建立的媒体播放器应用,源代码与 API 文档可以在在 GitHub 上访问。
该应用程序搜索用户可用的媒体服务器,包括连接到 PC 的媒体设备及通过网络管理媒体的软件。用户可以浏览媒体、通过网络播放或者离线保存。
如下是您使用 Sencha Ext JS 建立媒体播放器应用时必须做的几个关键步骤:
-
创建清单文件
manifest.json
。 -
创建事件页面
background.js
。 - 用沙盒屏蔽应用的逻辑。
- 在 Chrome 应用与经过沙盒屏蔽的文件之间通信。
- 搜索媒体服务器。
- 浏览与播放媒体。
- 离线保存媒体。
创建清单文件
所有 Chrome 应用都需要一个清单文件,包含 Chrome 浏览器执行应用时需要的信息。如清单文件中所述,媒体播放器应用是 "offline_enabled"(在离线状态下也能使用),媒体资源可以在本地保存、访问及播放,不管有没有网络连接。
"sandbox" 字段用来将应用的主要逻辑通过沙盒屏蔽在一个唯一的来源中,所有经过沙盒屏蔽的内容不受 Chrome 应用内容安全策略的限制,但也不能直接访问 Chrome 应用的 API。清单文件还包含了 "socket" 权限,因为媒体播放器应用使用套接字 API 通过网络连接到媒体服务器。
{ "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" ] } ] }
创建事件页面
所有 Chrome 应用都需要 background.js
来执行应用。媒体播放器的主页面 index.html
将会在指定大小的窗口中打开:
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; }); });
通过沙盒屏蔽应用的逻辑
Chrome 应用程序在一个受控制的环境中运行,强制实施严格的内容安全策略(CSP),而媒体播放器应用需要某些更高的权限来渲染 Ext JS
组件。为了遵循 CSP 的同时执行应用的逻辑,应用的主页面
index.html
创建了一个 iframe,作为一个经过沙盒屏蔽的环境:
<iframe id="sandbox-frame" sandbox="allow-scripts" src="sandbox.html"></iframe>
iframe 指向 sandbox.html,包含了 Ext JS 应用需要的文件:
<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>
app.js
脚本执行所有 Ext JS
代码,并渲染媒体播放器的视图。由于该脚本经过沙盒屏蔽,它无法直接访问 Chrome
应用的 API,但可以使用
HTML5
消息传递 API 实现 app.js
与不经过沙盒屏蔽的文件间的通信。
在文件之间通信
为了使媒体播放应用访问 Chrome
应用的 API,例如查询网络上的媒体服务器,app.js
向
index.js
传递消息。和经过沙盒屏蔽的 app.js
不同,index.js
可以直接访问 Chrome 应用 API。
index.js
获取框架:
var iframe = document.getElementById('sandbox-frame'); iframeWindow = iframe.contentWindow;
然后监听来自经过沙盒屏蔽的文件的消息:
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);
在以下例子中,app.js
向 index.js
发送消息,请求键为 'extension-baseurl':
Ext.data.PostMessage.request({ key: 'extension-baseurl', success: function(data) { //... } });
index.js
接收请求,设置结果,通过发回基本 URL 的方式回复:
function extensionBaseUrl(data) { data.result = chrome.extension.getURL('/'); iframeWindow.postMessage(data, '*'); }
搜索媒体服务器
搜索媒体服务器涉及到许多细节。从较高的层次来看,搜索的工作流程由用户搜索可用媒体服务器的操作发起,媒体服务器控制器向
index.js
发送消息,index.js
监听该消息,收到时调用
Upnp.js。
Upnp
库使用 Chrome 应用的套接字
API
将所有发现的媒体服务器连接到媒体播放器应用,并从媒体服务器接收媒体数据。Upnp.js
还使用 soapclient.js
分析媒体服务器的数据。这一节剩下的内容更详细地描述这一工作流程。
传递消息
当用户单击媒体播放器应用中央的媒体服务器按钮时,MediaServers
调用 discoverServers()
。该函数首先检查是否有正在进行的发现请求,如果有的话终止它们以便发起新的请求。接着,控制器向
index.js
传递消息,键为
upnp-discovery,并包含两个回调函数监听器:
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'); } });
调用 upnpDiscover()
index.js
监听来自 app.js
的 'upnp-discover' 消息,并通过调用
upnpDiscover()
响应。当发现某个媒体服务器时,index.js
从参数中提取媒体服务器的域名,在本地保存服务器,格式化媒体服务器数据,并将数据传递给 MediaServer
控制器。
分析媒体服务器数据
当 Upnp.js
发现新的媒体服务器时,它会获取设备的描述,并发出
Soaprequest 浏览并分析媒体服务器数据,soapclient.js
通过文档中的标签名称分析媒体元素。
连接到媒体服务器
Upnp.js
连接到发现的媒体服务器,并使用 Chrome 应用的套接字 API 接收媒体数据:
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); }); }); });
浏览和播放媒体
MediaExplorer 控制器列出媒体服务器文件夹中的所有媒体文件,并负责更新媒体播放器应用窗口中的面包屑导航。当用户选中媒体文件时,控制器向
index.js
传递消息,键为 'play-media'。
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 } }); } },
index.js
监听传递过来的该消息,并通过调用 playMedia()
响应。
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); }
离线保存媒体
离线保存媒体的大部分繁重任务由 filer.js 库来实现,您可以在 filer.js 简介中了解有关该库的更多内容。
当用户选择一个或多个文件,并执行
'Take offline'(离线保存)操作时,将开始这一过程。MediaExplorer 控制器向 index.js
传递消息,键为
'download-media'。index.js
监听该消息并调用
downloadMedia()
函数开始下载过程:
function downloadMedia(data) { DownloadProcess.run(data.params.files, function() { data.result = true; sendMessage(data); }); }
DownloadProcess
实用方法创建 XHR
请求从媒体服务器获取数据,并等待完成状态。然后调用 onload
回调函数,检查接收到的内容并使用 filer.js
函数在本地保存数据:
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); } );
下载过程完成后,MediaExplorer
更新媒体文件列表以及媒体播放器的树窗格。