该页面翻译自 Google Chrome Extensions 与 Google Chrome Apps。除非特别说明,该页面的内容遵循 Creative Commons Attribution 3.0 License,代码示例遵循 BSD License。
该文档的目的是使您初步了解如何使用 Sencha Ext JS 框架建立 Chrome 应用。要实现这一目的,我们将会深入讨论一个通过 Sencha 建立的媒体播放器应用,源代码与 API 文档可以在在 GitHub 上访问。
该应用程序搜索用户可用的媒体服务器,包括连接到 PC 的媒体设备及通过网络管理媒体的软件。用户可以浏览媒体、通过网络播放或者离线保存。
如下是您使用 Sencha Ext JS 建立媒体播放器应用时必须做的几个关键步骤:
manifest.json
。
background.js
。
所有 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'); } });
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
更新媒体文件列表以及媒体播放器的树窗格。