本文档旨在协助您开始使用 Sencha Ext JS 框架构建 Chrome 应用。为了实现这一目标,我们将深入探讨一款由 Sencha 构建的媒体播放器应用。GitHub 上提供了源代码和 API 文档。
此应用发现用户的可用媒体服务器,包括连接到 PC 的媒体设备和通过网络管理媒体的软件。用户可以浏览媒体、通过网络播放或离线保存。
如需使用 Sencha Ext JS 构建媒体播放器应用,您必须执行以下操作:
- 创建清单
manifest.json
。 - 创建活动页面:
background.js
。 - 沙盒应用的逻辑。
- 在 Chrome 应用和沙盒文件之间通信。
- 发现媒体服务器。
- 探索和播放媒体内容。
- 离线保存媒体内容。
创建清单
所有 Chrome 应用都需要一个清单文件,其中包含 Chrome 启动应用所需的信息。如清单所示,媒体播放器应用为“offline_enabled”;媒体素材资源可在本地保存、访问和播放,无论连接状况如何。
“sandbox”字段用于在独有的来源中将应用的主要逻辑沙盒化。所有沙盒化内容均不受 Chrome 应用内容安全政策约束,但无法直接访问 Chrome 应用 API。该清单还包含“套接字”权限;媒体播放器应用使用套接字 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。app.js
和非沙盒文件之间的通信是使用 HTML5 Post Message API 完成的。
在文件之间通信
为了让媒体播放器应用访问 Chrome 应用 API(例如查询网络以查找媒体服务器),app.js
会将消息发布到 index.js。与沙盒化 app.js
不同,index.js
可以直接访问 Chrome 应用 API。
index.js
会创建 iframe:
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
会接收请求、分配结果,并通过发回基准网址进行回复:
function extensionBaseUrl(data) {
data.result = chrome.extension.getURL('/');
iframeWindow.postMessage(data, '*');
}
发现媒体服务器
发现媒体服务器需要完成许多工作。概括来讲,发现工作流由用户操作启动,以搜索可用的媒体服务器。MediaServer 控制器将消息发布到 index.js
;index.js
会监听此消息,并在收到消息后调用 Upnp.js。
Upnp library
使用 Chrome 应用套接字 API 将媒体播放器应用与任何已发现的媒体服务器连接,并从媒体服务器接收媒体数据。Upnp.js
还会使用 soapclient.js 解析媒体服务器数据。本部分的其余内容将更详细地介绍此工作流。
发布消息
当用户点击媒体播放器应用中心的媒体服务器按钮时,MediaServers.js
会调用 discoverServers()
。此函数首先检查是否存在任何未完成的发现请求,如果为 true,则会中止这些请求,以便可以启动新请求。接下来,控制器会向 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);
});
});
});
探索和播放媒体内容
Media Explorer 控制器会列出媒体服务器文件夹内的所有媒体文件,并负责更新媒体播放器应用窗口中的面包屑导航导航。当用户选择媒体文件时,控制器会使用“play-media”键向 index.js
发布一条消息:
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( 'transitionend', animEnd, false );
animEnd = null;
};
//load media
mediaEl.src = url;
mediaEl.load();
//animate in player
playerCt.addEventListener( 'transitionend', animEnd, false );
playerCt.style.transform = "translateY(0)";
//reply postmessage
data.result = true;
sendMessage(data);
}
离线保存媒体内容
离线保存媒体内容的大部分工作都是由 filer.js 库完成的。如需详细了解此库,请参阅 filer.js 简介。
当用户选择一个或多个文件并执行“离线使用”操作时,此过程便会开始。
Media Explorer 控制器使用键“download-media”向 index.js
发布一条消息;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
会更新媒体文件列表和媒体播放器树面板。