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

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

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

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

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

สร้างไฟล์ Manifest

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

ช่อง "แซนด์บ็อกซ์" ใช้สำหรับแซนด์บ็อกซ์ลอจิกหลักของแอปในต้นทางที่ไม่ซ้ำกัน เนื้อหาทั้งหมดที่อยู่ในแซนด์บ็อกซ์จะได้รับการยกเว้นจากนโยบายรักษาความปลอดภัยเนื้อหาของแอป 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

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

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

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() ก่อนอื่น ฟังก์ชันนี้จะตรวจสอบคำขอการค้นพบที่ยังเหลืออยู่ และหากเป็นจริง จะล้มเลิกคำขอดังกล่าวเพื่อให้เริ่มคำขอใหม่ได้ ถัดไป ตัวควบคุมจะโพสต์ข้อความไปยัง index.js พร้อม upnp-discovery ที่สำคัญ และ Listener โค้ดเรียกกลับ 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 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 จะแสดงไฟล์สื่อทั้งหมดในโฟลเดอร์เซิร์ฟเวอร์สื่อ มีหน้าที่อัปเดตการนำทางเบรดครัมบ์ในหน้าต่างแอปโปรแกรมเล่นสื่อ เมื่อผู้ใช้เลือกไฟล์สื่อ ตัวควบคุมจะโพสต์ข้อความไปยัง 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

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