使用 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。「app.js」之間的通訊 非沙箱化檔案則是使用 HTML5 Post Message API 來完成。

在檔案間進行通訊

為了讓媒體播放器應用程式存取 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 會收到要求、指派結果,並透過傳回 Base 網址進行回覆:

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,請取消這些動作,以啟動新的要求。接著,控制器會張貼訊息 index.js 具有一個鍵 upnp-discovery,以及兩個回呼事件監聽器:

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 控制器列出了媒體伺服器資料夾中的所有媒體檔案, 負責更新媒體播放器應用程式視窗中的導覽標記導覽。當使用者 選取媒體檔案後,控制器會透過「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.jsindex.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 會更新媒體檔案清單和媒體 播放器樹狀結構面板