สร้างแอปด้วย Sencha Ext JS

เป้าหมายของเอกสารนี้คือการช่วยคุณเริ่มสร้างแอป Chrome ด้วย Sencha Ext JS เราจะเจาะลึกเกี่ยวกับแอปมีเดียเพลเยอร์ที่ Sencha สร้างขึ้นเพื่อให้บรรลุเป้าหมายนี้ แหล่งข้อมูล โค้ดและเอกสารประกอบ API จะอยู่ใน GitHub

แอปนี้ค้นพบเซิร์ฟเวอร์สื่อที่ผู้ใช้สามารถใช้ได้ รวมถึงอุปกรณ์สื่อที่เชื่อมต่อกับพีซีและ ซึ่งเป็นซอฟต์แวร์ที่จัดการสื่อผ่านเครือข่าย ผู้ใช้สามารถเรียกดูสื่อ เล่นผ่านเครือข่าย หรือบันทึกได้ ออฟไลน์อยู่

สิ่งสำคัญที่คุณต้องดำเนินการเพื่อสร้างแอปมีเดียเพลเยอร์โดยใช้ Sencha Ext JS มีดังนี้

  • สร้างไฟล์ Manifest manifest.json
  • สร้างหน้ากิจกรรม background.js
  • ตรรกะของแอปในแซนด์บ็อกซ์
  • สื่อสารระหว่างแอป Chrome กับไฟล์ที่แซนด์บ็อกซ์
  • สำรวจเซิร์ฟเวอร์สื่อ
  • สำรวจและเล่นสื่อ
  • บันทึกสื่อแบบออฟไลน์

สร้างไฟล์ Manifest

แอป Chrome ทั้งหมดต้องมีไฟล์ Manifest ซึ่งมีข้อมูลที่ Chrome จำเป็นต้องเปิดใช้งาน แอป แอปมีเดียเพลเยอร์มีสถานะเป็น "ออฟไลน์_enabled" ตามที่ระบุไว้ในไฟล์ Manifest องค์ประกอบสื่ออาจเป็น บันทึกในเครื่อง รวมทั้งเข้าถึงและเล่นได้โดยไม่ต้องมีการเชื่อมต่ออินเทอร์เน็ต

"แซนด์บ็อกซ์" จะถูกใช้เพื่อแซนด์บ็อกซ์ตรรกะหลักของแอปในต้นทางที่ไม่ซ้ำ ใช้แซนด์บ็อกซ์ทั้งหมด เนื้อหาจะได้รับการยกเว้นจากนโยบายรักษาความปลอดภัยเนื้อหาของแอป Chrome แต่จะเข้าถึงโดยตรงไม่ได้ API ของแอป Chrome ไฟล์ Manifest ยังมี "socket" การอนุญาต; แอปมีเดียเพลเยอร์ใช้ 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 ทั้งหมดและแสดงมุมมองมีเดียเพลเยอร์ เนื่องจาก สคริปต์ถูกแซนด์บ็อกซ์ ทำให้ไม่สามารถเข้าถึง API ของแอป Chrome ได้โดยตรง การสื่อสารระหว่าง app.js และไฟล์ที่ไม่ใช่แซนด์บ็อกซ์นั้นใช้ HTML5 Post Message API

สื่อสารระหว่างไฟล์

เพื่อให้แอปมีเดียเพลเยอร์เข้าถึง API ของแอป Chrome ได้ เช่น ค้นหาเครือข่ายสำหรับสื่อ เซิร์ฟเวอร์ app.js โพสต์ข้อความไปยัง index.js index.js ต่างจาก app.js แบบแซนด์บ็อกซ์ ตรงที่สามารถ เข้าถึง API ของแอป Chrome ได้โดยตรง

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 จะรับคำขอ กำหนดผลลัพธ์ และตอบกลับโดยส่ง URL หลักกลับไป

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

สำรวจเซิร์ฟเวอร์สื่อ

การค้นพบเซิร์ฟเวอร์สื่อนั้นมีอยู่หลายขั้นตอน ในระดับสูง เวิร์กโฟลว์การสำรวจ เริ่มต้นจากการดำเนินการของผู้ใช้เพื่อค้นหาเซิร์ฟเวอร์สื่อที่ใช้ได้ ตัวควบคุม MediaServer โพสต์ข้อความถึง index.js; index.js จะรอฟังข้อความนี้ และเมื่อได้รับสาย Upnp.js

Upnp library ใช้ socket API ของแอป Chrome เพื่อเชื่อมต่อแอปมีเดียเพลเยอร์กับ ค้นพบเซิร์ฟเวอร์สื่อและรับข้อมูลสื่อจากเซิร์ฟเวอร์สื่อ Upnp.js ยังใช้ soapclient.js เพื่อแยกวิเคราะห์ข้อมูลเซิร์ฟเวอร์สื่อ ส่วนที่เหลือของส่วนนี้อธิบายถึง เวิร์กโฟลว์อย่างละเอียดมากขึ้น

โพสต์ข้อความ

เมื่อผู้ใช้คลิกปุ่มเซิร์ฟเวอร์สื่อตรงกลางแอปมีเดียเพลเยอร์ MediaServers.js โทรหา discoverServers() ฟังก์ชันนี้จะตรวจสอบคำขอค้นพบที่ค้างอยู่ก่อน หาก true ล้มเลิกเพื่อเริ่มต้นคำขอใหม่ จากนั้น ผู้ควบคุมข้อมูลจะโพสต์ข้อความถึง index.js ที่มีการค้นพบ Upnp คีย์และ Listener ของ Callback 2 รายการ ดังนี้

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 พร้อมข้อความ "เล่นสื่อ" คีย์:

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 จะโพสต์ข้อความไปยัง index.js ด้วยคีย์ "download-media" index.js จะรอฟังข้อความนี้และเรียกฟังก์ชัน downloadMedia() เพื่อเริ่มต้น ขั้นตอนการดาวน์โหลด:

function downloadMedia(data) {
        DownloadProcess.run(data.params.files, function() {
            data.result = true;
            sendMessage(data);
        });
    }

วิธียูทิลิตี DownloadProcess จะสร้างคำขอ xhr เพื่อรับข้อมูลจากเซิร์ฟเวอร์สื่อและ กำลังรอสถานะเสร็จสมบูรณ์ การดำเนินการนี้จะเริ่มต้น Callback 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 จะอัปเดตรายการไฟล์สื่อและสื่อ แผงแผนผังโปรแกรมเล่น