تصميم تطبيقات باستخدام Sencha Ext JS

الهدف من هذا المستند هو مساعدتك على البدء في إنشاء تطبيقات Chrome باستخدام إطار عمل Sencha Ext JS. ولتحقيق هذا الهدف، سوف نتعمق في تطبيق مشغّل الوسائط الذي أنشأه سينشا. يتوفّر رمز المصدر ووثائق واجهة برمجة التطبيقات على GitHub.

يكتشف هذا التطبيق خوادم الوسائط المتاحة للمستخدم، بما في ذلك أجهزة الوسائط المتصلة بالكمبيوتر والبرامج التي تدير الوسائط عبر الشبكة. يمكن للمستخدمين تصفح الوسائط أو تشغيلها عبر الشبكة أو حفظها في وضع عدم الاتصال.

في ما يلي الخطوات الأساسية التي يجب تنفيذها لإنشاء تطبيق مشغّل وسائط باستخدام Sencha Ext JS:

  • إنشاء ملف البيان، manifest.json.
  • إنشاء صفحة الحدث، background.js.
  • منطق تطبيق Sandbox.
  • الاتصال بين تطبيق Chrome والملفات المحمية في وضع الحماية
  • اكتشاف خوادم الوسائط
  • استكشاف الوسائط وتشغيلها
  • حفظ الوسائط بلا اتصال بالإنترنت

إنشاء البيان

تتطلب جميع تطبيقات Chrome ملف بيان يحتوي على المعلومات التي يحتاجها Chrome لتشغيل التطبيقات. كما هو موضح في ملف البيان، فإن تطبيق مشغّل الوسائط "offline_enabled" (قد لا يتم الاتصال بالإنترنت)، يمكن حفظ مواد عرض الوسائط محليًا، والوصول إليها وتشغيلها بغض النظر عن الاتصال بالإنترنت.

يُستخدم حقل "وضع الحماية" لتوفير وضع الحماية للمنطق الرئيسي للتطبيق في مصدر فريد. يتم إعفاء كل المحتوى المحمي في وضع الحماية من سياسة أمان المحتوى لتطبيقات Chrome، ولكن لا يمكنه الوصول مباشرةً إلى واجهات برمجة تطبيقات تطبيقات Chrome. ويتضمن البيان أيضًا إذن "المقبس"، ويستخدم تطبيق مشغّل الوسائط واجهة برمجة تطبيقاتsocket للاتصال بخادم وسائط عبر الشبكة.

{
    "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) صارمة. يحتاج تطبيق مشغّل الوسائط إلى بعض الامتيازات الأعلى لعرض مكوّنات JavaScript. للتوافق مع سياسة أمان المحتوى (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. يتم التواصل بين الملفات app.js والملفات التي لا تتضمن وضع الحماية باستخدام واجهة برمجة التطبيقات لمشاركة رسائل HTML5.

التواصل بين الملفات

يرسل app.js الرسائل إلى index.js ليتمكن تطبيق مشغّل الوسائط من الوصول إلى واجهات برمجة تطبيقات Chrome، مثل الاستعلام عن الشبكة عن خوادم الوسائط. على عكس app.js الموضوع في وضع الحماية، يمكن لـ index.js الوصول مباشرةً إلى واجهات برمجة تطبيقات تطبيقات 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(). تتحقّق هذه الدالة أولاً من وجود أي طلبات اكتشاف مُعلّقة، وإذا كانت القيمة "صحيح"، فإنها تلغيها حتى يمكن بدء الطلب الجديد. بعد ذلك، تنشر وحدة التحكّم رسالة على "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 خادم وسائط جديدًا، يسترد بعد ذلك وصفًا للجهاز ويرسل طلب صابون لتصفّح بيانات خادم الوسائط وتحليلها، ويحلِّل soapclient.js عناصر الوسائط حسب اسم العلامة في مستند.

الاتصال بخادم الوسائط

يتصل Upnp.js بخوادم الوسائط التي تم اكتشافها ويتلقى بيانات الوسائط باستخدام واجهة برمجة التطبيقات Chrome App Socket:

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 قائمة ملفات الوسائط ولوحة العرض التدرّجي لمشغّل الوسائط.