이 문서의 목표는 Sencha Ext JS 프레임워크를 사용하여 Chrome 앱을 빌드하려는 사용자를 돕는 것입니다. 이 목표를 달성하기 위해 Sencha에서 빌드한 미디어 플레이어 앱을 살펴보겠습니다. 소스 코드 및 API 문서는 GitHub에서 확인할 수 있습니다.
이 앱은 PC에 연결된 미디어 기기 및 네트워크를 통해 미디어를 관리하는 소프트웨어를 비롯하여 사용자의 사용 가능한 미디어 서버를 검색합니다. 사용자는 미디어를 탐색하거나, 네트워크를 통해 재생하거나, 오프라인에 저장할 수 있습니다.
다음은 Sencha Ext JS를 사용하여 미디어 플레이어 앱을 빌드하기 위해 수행해야 하는 주요 작업입니다.
- 매니페스트(
manifest.json
)를 만듭니다. - 이벤트 페이지(
background.js
)를 만듭니다. - Sandbox 앱의 로직
- Chrome 앱과 샌드박스 파일 간에 통신합니다.
- 미디어 서버를 검색합니다.
- 미디어를 탐색하고 재생합니다.
- 미디어를 오프라인에 저장합니다.
매니페스트 만들기
모든 Chrome 앱에는 Chrome에서 앱을 실행하는 데 필요한 정보가 포함된 매니페스트 파일이 필요합니다. 매니페스트에 표시된 대로 미디어 플레이어 앱은 'offline_enabled'입니다. 미디어 애셋은 연결과 관계없이 로컬에 저장하고 액세스하고 재생할 수 있습니다.
'샌드박스' 필드는 고유한 출처에서 앱의 기본 로직을 샌드박스에 추가하는 데 사용됩니다. 샌드박스 처리된 모든 콘텐츠는 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은 Ext JS 애플리케이션에 필요한 파일이 포함된 sandbox.html을 가리킵니다.
<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에 직접 액세스할 수 없습니다. 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
가 요청을 수신하고 결과를 할당한 후 Base URL을 다시 전송하여 응답합니다.
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인 경우 새 요청이 시작될 수 있도록 요청을 취소합니다. 그런 다음 컨트롤러는 키 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);
});
});
});
미디어 탐색 및 재생
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 요청을 만들어 미디어 서버에서 데이터를 가져오고 완료 상태를 기다립니다. 이렇게 하면 수신된 콘텐츠를 확인하고 filer.js
함수를 사용하여 데이터를 로컬에 저장하는 onload 콜백이 시작됩니다.
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
는 미디어 파일 목록과 미디어 플레이어 트리 패널을 업데이트합니다.