本文件旨在協助您開始使用 Sencha Ext JS 架構建構 Chrome 應用程式。為達成這個目標,我們將深入探討 Sencha 打造的媒體播放器應用程式。您可以在 GitHub 上找到原始碼和 API 說明文件。
這個應用程式會探索使用者可用的媒體伺服器,包括連接到電腦連線的媒體裝置,以及透過網路管理媒體的軟體。使用者可以瀏覽媒體、透過網路播放,或儲存離線內容。
如要使用 Sencha Ext JS 建構媒體播放器應用程式,您必須採取以下重要措施:
- 建立資訊清單「
manifest.json
」。 - 建立活動頁面 (
background.js
)。 - 沙箱應用程式的邏輯。
- Chrome 應用程式和沙箱檔案之間可以進行通訊。
- 探索媒體伺服器。
- 探索及播放媒體。
- 將媒體儲存到離線觀看清單。
建立資訊清單
所有 Chrome 應用程式都需要資訊清單檔案,其中包含 Chrome 啟動應用程式所需的資訊。如資訊清單所示,媒體播放器應用程式為「Offline_enabled」,但媒體資產可以儲存在本機、存取及播放,不受連線能力影響。
「沙箱」欄位的用途,採用沙箱機制,掌握應用程式在不重複來源中的主要邏輯。所有沙箱內容都不受 Chrome 應用程式內容安全政策的限制,但無法直接存取 Chrome App 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 App API。系統會使用 HTML5 Post Message API 完成 app.js
和非採用沙箱機制的檔案之間的通訊。
檔案之間進行通訊
為了讓媒體播放器應用程式存取 Chrome 應用程式 API,例如查詢媒體伺服器的網路,app.js
會將訊息發布至 index.js。與採用沙箱機制的 app.js
不同,index.js
可以直接存取 Chrome App 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 應用程式 socket API 將媒體播放器應用程式與任何偵測到的媒體伺服器連線,並從媒體伺服器接收媒體資料。Upnp.js
也會使用 soapclient.js 剖析媒體伺服器資料。本節的其餘部分會詳細說明這個工作流程。
張貼訊息
當使用者按一下媒體播放器應用程式中央的「媒體伺服器」按鈕時,MediaServers.js
會呼叫 discoverServers()
。這個函式會先檢查是否有任何尚未完成的探索要求,如果為 true,則會取消這些要求,以便啟動新的要求。接著,控制器會使用 Key upnp-discovery 和兩個回呼事件監聽器,將訊息發布至 index.js
:
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 App socket 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 控制器會列出媒體伺服器資料夾中的所有媒體檔案,負責更新媒體播放器應用程式視窗中的導覽標記導覽。當使用者選取媒體檔案時,控制器會使用「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 中進一步瞭解這個程式庫。
只要使用者選取一或多個檔案,並執行「離線取得」動作,這項程序就會開始執行。
MediaExplorer 控制器會使用「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
會更新媒體檔案清單和媒體播放器樹狀結構面板。