เป้าหมายของเอกสารนี้คือการช่วยคุณเริ่มสร้างแอป Chrome ด้วย Sencha Ext JS เราจะเจาะลึกเกี่ยวกับแอปมีเดียเพลเยอร์ที่ Sencha สร้างขึ้นเพื่อให้บรรลุเป้าหมายนี้ แหล่งข้อมูล โค้ดและเอกสารประกอบ API จะอยู่ใน GitHub
แอปนี้ค้นพบเซิร์ฟเวอร์สื่อที่ผู้ใช้สามารถใช้ได้ รวมถึงอุปกรณ์สื่อที่เชื่อมต่อกับพีซีและ ซึ่งเป็นซอฟต์แวร์ที่จัดการสื่อผ่านเครือข่าย ผู้ใช้สามารถเรียกดูสื่อ เล่นผ่านเครือข่าย หรือบันทึกได้ ออฟไลน์อยู่
สิ่งสำคัญที่คุณต้องดำเนินการเพื่อสร้างแอปมีเดียเพลเยอร์โดยใช้ Sencha Ext JS มีดังนี้
- สร้างไฟล์ Manifest
manifest.json
- สร้างหน้ากิจกรรม
background.js
- ตรรกะของแอปในแซนด์บ็อกซ์
- สื่อสารระหว่างแอป Chrome กับไฟล์ที่แซนด์บ็อกซ์
- สำรวจเซิร์ฟเวอร์สื่อ
- สำรวจและเล่นสื่อ
- บันทึกสื่อแบบออฟไลน์
สร้างไฟล์ Manifest
แอป Chrome ทั้งหมดต้องมีไฟล์ Manifest ซึ่งมีข้อมูลที่ Chrome จำเป็นต้องเปิดใช้งาน แอป แอปมีเดียเพลเยอร์มีสถานะเป็น "ออฟไลน์_enabled" ตามที่ระบุไว้ในไฟล์ Manifest องค์ประกอบสื่ออาจเป็น บันทึกในเครื่อง รวมทั้งเข้าถึงและเล่นได้โดยไม่ต้องมีการเชื่อมต่ออินเทอร์เน็ต
"แซนด์บ็อกซ์" จะถูกใช้เพื่อแซนด์บ็อกซ์ตรรกะหลักของแอปในต้นทางที่ไม่ซ้ำ ใช้แซนด์บ็อกซ์ทั้งหมด เนื้อหาจะได้รับการยกเว้นจากนโยบายรักษาความปลอดภัยเนื้อหาของแอป Chrome แต่จะเข้าถึงโดยตรงไม่ได้ API ของแอป Chrome ไฟล์ Manifest ยังมี "socket" การอนุญาต; แอปมีเดียเพลเยอร์ใช้ 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 ทั้งหมดและแสดงมุมมองมีเดียเพลเยอร์ เนื่องจาก
สคริปต์ถูกแซนด์บ็อกซ์ ทำให้ไม่สามารถเข้าถึง API ของแอป Chrome ได้โดยตรง การสื่อสารระหว่าง app.js
และไฟล์ที่ไม่ใช่แซนด์บ็อกซ์นั้นใช้ HTML5 Post Message API
สื่อสารระหว่างไฟล์
เพื่อให้แอปมีเดียเพลเยอร์เข้าถึง API ของแอป Chrome ได้ เช่น ค้นหาเครือข่ายสำหรับสื่อ
เซิร์ฟเวอร์ app.js
โพสต์ข้อความไปยัง index.js index.js
ต่างจาก app.js
แบบแซนด์บ็อกซ์ ตรงที่สามารถ
เข้าถึง API ของแอป Chrome ได้โดยตรง
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
จะรับคำขอ กำหนดผลลัพธ์ และตอบกลับโดยส่ง URL หลักกลับไป
function extensionBaseUrl(data) {
data.result = chrome.extension.getURL('/');
iframeWindow.postMessage(data, '*');
}
สำรวจเซิร์ฟเวอร์สื่อ
การค้นพบเซิร์ฟเวอร์สื่อนั้นมีอยู่หลายขั้นตอน ในระดับสูง เวิร์กโฟลว์การสำรวจ
เริ่มต้นจากการดำเนินการของผู้ใช้เพื่อค้นหาเซิร์ฟเวอร์สื่อที่ใช้ได้ ตัวควบคุม MediaServer
โพสต์ข้อความถึง index.js
; index.js
จะรอฟังข้อความนี้ และเมื่อได้รับสาย
Upnp.js
Upnp library
ใช้ socket API ของแอป Chrome เพื่อเชื่อมต่อแอปมีเดียเพลเยอร์กับ
ค้นพบเซิร์ฟเวอร์สื่อและรับข้อมูลสื่อจากเซิร์ฟเวอร์สื่อ Upnp.js
ยังใช้
soapclient.js เพื่อแยกวิเคราะห์ข้อมูลเซิร์ฟเวอร์สื่อ ส่วนที่เหลือของส่วนนี้อธิบายถึง
เวิร์กโฟลว์อย่างละเอียดมากขึ้น
โพสต์ข้อความ
เมื่อผู้ใช้คลิกปุ่มเซิร์ฟเวอร์สื่อตรงกลางแอปมีเดียเพลเยอร์ MediaServers.js
โทรหา discoverServers()
ฟังก์ชันนี้จะตรวจสอบคำขอค้นพบที่ค้างอยู่ก่อน หาก
true ล้มเลิกเพื่อเริ่มต้นคำขอใหม่ จากนั้น ผู้ควบคุมข้อมูลจะโพสต์ข้อความถึง
index.js
ที่มีการค้นพบ Upnp คีย์และ Listener ของ Callback 2 รายการ ดังนี้
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
ฟัง "upnp-discover" ข้อความจาก app.js
และตอบด้วยการโทร
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
พร้อมข้อความ "เล่นสื่อ" คีย์:
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
กระบวนการนี้จะเริ่มต้นเมื่อผู้ใช้เลือกไฟล์อย่างน้อย 1 ไฟล์และเริ่ม "ดูแบบออฟไลน์" การดำเนินการ
ตัวควบคุม MediaExplorer จะโพสต์ข้อความไปยัง index.js
ด้วยคีย์ "download-media"
index.js
จะรอฟังข้อความนี้และเรียกฟังก์ชัน downloadMedia()
เพื่อเริ่มต้น
ขั้นตอนการดาวน์โหลด:
function downloadMedia(data) {
DownloadProcess.run(data.params.files, function() {
data.result = true;
sendMessage(data);
});
}
วิธียูทิลิตี DownloadProcess
จะสร้างคำขอ xhr เพื่อรับข้อมูลจากเซิร์ฟเวอร์สื่อและ
กำลังรอสถานะเสร็จสมบูรณ์ การดำเนินการนี้จะเริ่มต้น Callback 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
จะอัปเดตรายการไฟล์สื่อและสื่อ
แผงแผนผังโปรแกรมเล่น