使用 Sencha Ext JS 建構應用程式

本文件旨在協助您開始使用 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.jsindex.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 會更新媒體檔案清單和媒體播放器樹狀結構面板。