使用 Sencha Ext JS 构建应用

本文档旨在帮助您开始使用 Sencha Ext JS 构建 Chrome 应用 框架。为实现此目标,我们将深入探索一款由 Sencha 构建的媒体播放器应用。来源 代码API 文档

此应用可发现用户的可用媒体服务器,包括连接到 PC 的媒体设备以及 用于管理网络媒体的软件。用户可以浏览媒体内容、通过网络播放 离线使用。

要使用 Sencha Ext JS 构建媒体播放器应用,您必须执行以下关键操作:

  • 创建清单 manifest.json
  • 创建活动页面background.js
  • 沙盒应用的逻辑。
  • 在 Chrome 应用和沙盒化文件之间进行通信。
  • 发现媒体服务器。
  • 浏览和播放媒体内容。
  • 离线保存媒体内容。

创建清单

所有 Chrome 应用都需要提供清单文件,其中包含 Chrome 启动所需的信息 。如该清单所示,媒体播放器应用为“offline_enabled”;媒体资源 即可在本地保存、访问和播放,而不受网络连接情况的影响。

“沙盒”字段用于沙盒化的唯一源中应用的主要逻辑。所有沙盒 内容不受 Chrome 应用内容安全政策的约束,但是无法直接访问 Chrome 应用 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 应用 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.jsindex.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.js 发布一条消息;index.js会收听这条短信,并会在收到短信后拨打电话 Upnp.js

Upnp library 使用 Chrome App socket API 将媒体播放器应用与任何 发现媒体服务器并从媒体服务器接收媒体数据。Upnp.js 还使用了 soapclient.js 来解析媒体服务器数据。本部分的其余内容将介绍 工作流。

发布消息

当用户点击媒体播放器应用中心的“媒体服务器”按钮时,MediaServers.js 调用 discoverServers()。此函数首先检查是否有未完成的发现请求,以及 则取消它们,以便发起新的请求。接下来,控制器会向 带有键 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 会监听“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 发布包含“play-media”的消息键:

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 控制器会向 index.js 发布一条消息,其中包含键“download-media”; 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 会更新媒体文件列表和媒体 播放器树面板中