Sencha Ext JS でアプリを作成する

このドキュメントは、Sencha Ext JS フレームワークを使用して Chrome アプリを構築できるようにすることを目的としています。この目標を達成するために、Sencha が開発したメディア プレーヤー アプリについて詳しく説明します。ソースコードAPI ドキュメントは GitHub で入手できます。

このアプリは、PC に接続されているメディア デバイスや、ネットワーク経由でメディアを管理するソフトウェアなど、ユーザーが利用可能なメディア サーバーを検出します。ユーザーはメディアのブラウジング、ネットワーク経由の再生、オフラインへの保存ができます。

Sencha Ext JS を使用してメディア プレーヤー アプリを作成する際の主なステップは次のとおりです。

  • マニフェスト manifest.json を作成します。
  • イベントページbackground.js)を作成します。
  • サンドボックス アプリのロジック。
  • Chrome アプリとサンドボックス化されたファイル間で通信する。
  • メディア サーバーを検出します。
  • メディアの検索と再生ができます。
  • オフラインにメディアを保存します。

マニフェストを作成する

すべての Chrome アプリでは、Chrome がアプリを起動するために必要な情報を含むマニフェスト ファイルが必要です。マニフェストに示されているように、メディア プレーヤー アプリは「offline_enabled」です。メディア アセットは、接続状況に関係なくローカルに保存し、アクセスして再生できます。

「sandbox」フィールドは、アプリのメインロジックを固有のオリジンにサンドボックス化するために使用します。サンドボックス化されたコンテンツはすべて 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 が 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 アプリの 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 はリクエストを受け取り、結果を割り当て、ベース URL を返信して応答します。

function extensionBaseUrl(data) {
    data.result = chrome.extension.getURL('/');
    iframeWindow.postMessage(data, '*');
}

メディア サーバーを検出する

メディア サーバーの検出には、さまざまな処理を行います。大まかに言うと、検出ワークフローは、利用可能なメディア サーバーを検索するユーザー アクションによって開始されます。MediaServer コントローラは、メッセージを index.js に送信します。index.js はこのメッセージをリッスンし、受信すると Upnp.js を呼び出します。

Upnp library は Chrome アプリの Socket API を使用して、検出したメディア サーバーにメディア プレーヤー アプリを接続し、そのメディア サーバーからメディアデータを受信します。また、Upnp.jssoapclient.js を使用してメディア サーバーのデータを解析します。このセクションの残りの部分では、このワークフローについて詳しく説明します。

メッセージを投稿

ユーザーがメディア プレーヤー アプリの中央にあるメディアサーバー ボタンをクリックすると、MediaServers.jsdiscoverServers() を呼び出します。この関数は、まず未処理の検出リクエストがあるかどうかを確認し、true の場合、新しいリクエストを開始できるようにそのリクエストを中止します。次に、コントローラはキー upnp-discovery と 2 つのコールバック リスナーを含むメッセージを 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 コントローラに push します。

メディア サーバーのデータを解析する

Upnp.js は新しいメディア サーバーを検出すると、デバイスの説明を取得し、メディア サーバーのデータの参照と解析のために Soap リクエストを送信します。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 の概要をご覧ください。

ユーザーが 1 つ以上のファイルを選択し、「オフラインで使用する」操作を開始すると、プロセスが開始します。 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 はメディア ファイル リストとメディア プレーヤーのツリーパネルを更新します。