בניית אפליקציות עם Sancha Ext JS

מטרת המסמך הזה היא לעזור לך להתחיל לפתח אפליקציות Chrome באמצעות Sencha Ext JS . כדי להשיג את המטרה הזו, נתעמק באפליקציה של נגן מדיה שפותחה על ידי Sencha. הקוד code ומסמכי תיעוד API זמינים ב-GitHub.

האפליקציה הזו מזהה את שרתי המדיה הזמינים של המשתמש, כולל מכשירי מדיה שמחוברים למחשב וגם תוכנה שמנהלת מדיה דרך הרשת. המשתמשים יכולים לדפדף במדיה, להפעיל תכנים דרך הרשת או לשמור במצב אופליין.

אלה הפעולות העיקריות שצריך לעשות כדי ליצור אפליקציה של נגן מדיה באמצעות Sencha Ext JS:

  • יצירת מניפסט, manifest.json.
  • יצירת דף אירוע, background.js.
  • לוגיקת האפליקציה Sandbox.
  • תקשורת בין אפליקציית Chrome לקבצים בארגז חול (sandbox).
  • מגלים שרתי מדיה.
  • חיפוש והפעלה של מדיה.
  • שמירת המדיה במצב אופליין.

יצירת מניפסט

לכל אפליקציות Chrome נדרש קובץ מניפסט שמכיל את המידע שדרוש ל-Chrome כדי להפעיל באפליקציות. כפי שמצוין במניפסט, האפליקציה של נגן המדיה במצב "אופליין_enabled"; נכסי מדיה יכולים להיות נשמר באופן מקומי, ניגש אליו ומופעל ללא קשר לקישוריות.

את "ארגז החול" משמש להרצה בארגז חול ללוגיקה הראשית של האפליקציה במקור ייחודי. כל ההרצה בארגז החול התוכן פטור ממדיניות אבטחת התוכן של אפליקציית Chrome, אבל הוא לא יכול לגשת ישירות ממשקי API של אפליקציות Chrome. המניפסט כולל גם את ה-"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;
    });

});

הלוגיקה של אפליקציית Sandbox

אפליקציות Chrome פועלות בסביבה מבוקרת שאוכפת מדיניות אבטחת תוכן מחמירה (CSP). לאפליקציית נגן המדיה נדרשות כמה הרשאות גבוהות יותר כדי לעבד את רכיבי Ext JS. שפת תרגום לציית ל-CSP ולהפעיל את לוגיקת האפליקציה, הדף הראשי של האפליקציה, index.html, שיוצר iframe פועלת כסביבת Sandbox:

<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 ומעבד את התצוגות של נגן המדיה. מאחר ש הסקריפט נמצא בארגז חול (sandbox), הוא לא יכול לגשת ישירות לממשקי ה-API של אפליקציות Chrome. התקשורת בין app.js וקבצים שאינם בארגז חול (sandbox) מתבצעים באמצעות HTML5 Post Message API.

תקשורת בין קבצים

כדי שאפליקציית נגן המדיה תוכל לגשת לממשקי API של אפליקציות Chrome, כמו שליחת שאילתות לרשת לגבי מדיה שרתים, app.js מפרסם הודעות ב-index.js. בניגוד ל-app.js שבארגז החול, index.js יכול לגשת ישירות לממשקי ה-API של אפליקציות Chrome.

ה-iframe נוצר על ידי index.js:

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-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-discovery' הודעה מאת 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 מציג את כל קובצי המדיה שנמצאים בתיקיית שרת המדיה, אחראי לעדכון הניווט של נתיבי הניווט בחלון של אפליקציית המדיה של Google. כשמשתמש בוחר קובץ מדיה, הבקר מפרסם הודעה אל 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 כדי לקבל נתונים משרת המדיה ממתין לסטטוס ההשלמה. מופעלת קריאה חוזרת (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 מעדכן את רשימת קובצי המדיה ואת פריט המדיה חלונית עץ הנגן.