Sencha Ext JS로 앱 빌드

이 문서의 목표는 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.jsindex.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.jsindex.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.jssoapclient.js를 사용하여 미디어 서버 데이터를 파싱합니다. 이 섹션의 나머지 부분에서는 이 워크플로를 자세히 설명합니다

메시지 게시

사용자가 미디어 플레이어 앱 중앙에 있는 미디어 서버 버튼을 클릭하면 MediaServers.jsdiscoverServers()를 호출합니다. 이 함수는 먼저 대기 중인 검색 요청을 확인하고 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.jsapp.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는 미디어 파일 목록과 미디어 플레이어 트리 패널을 업데이트합니다.