في العامين الماضيين، ساعدتُ بعض الشركات المختلفة في تحقيق وظائف تشبه مشاركة الشاشة باستخدام تقنيات المتصفّح فقط. من واقع خبرتي، فإنّ تنفيذ VNC فقط في تكنولوجيات منصات الويب (أي بدون مكوّنات إضافية) هو مشكلة صعبة. هناك الكثير من الأشياء التي يجب مراعاتها والكثير من التحديات التي يجب التغلب عليها. ومن بين هذه المشاكل نقل موضع مؤشر الماوس وإعادة توجيه ضغطات المفاتيح وتحقيق إعادة رسم كاملة للألوان بمعدل 24 بت وبمعدل 60 لقطة في الثانية.
جارٍ تسجيل محتوى علامة التبويب
إذا أزلنا التعقيدات المرتبطة بمشاركة الشاشة التقليدية وركّزنا على مشاركة محتوى علامة تبويب المتصفّح، ستصبح المشكلة أبسط بكثير، إذ تقتصر على: أ) التقاط علامة التبويب المرئية في حالتها الحالية، و ب) إرسال ذلك "الإطار" عبر السلك. نحتاج في الأساس إلى طريقة لالتقاط لقطة من نموذج DOM ومشاركتها.
جزء المشاركة سهل. تتمتع Websockets بقدرة كبيرة على إرسال البيانات بتنسيقات مختلفة (سلسلة، JSON، ثنائي). أما الجزء المتعلق باللقطات، فيُعدّ مشكلة أصعب بكثير. تعاملت مشاريع مثل html2canvas مع التقاط لقطات شاشة لصفحات HTML من خلال إعادة تنفيذ محرّك العرض في المتصفّح… باستخدام JavaScript. ومن الأمثلة الأخرى تعليقات Google، مع أنّها ليست مفتوحة المصدر. هذه الأنواع من المشاريع رائعة جدًا، ولكنها أيضًا بطيئة للغاية. ستكون محظوظًا إذا حصلت على معدل نقل بيانات يبلغ إطارًا واحدًا في الثانية، ناهيك عن معدل 60 إطارًا في الثانية الذي ترغب فيه.
تتناول هذه المقالة بعض الحلول المفضّلة لديّ لإثبات جدوى الفكرة في ما يتعلّق بـ "مشاركة الشاشة" لعلامة تبويب.
الطريقة الأولى: Mutation Observers + WebSocket
عرض +Rafael Weinstein في وقت سابق من هذا العام طريقة واحدة لنسخ علامة تبويب. تستخدم طريقته Mutation Observers وWebSocket.
بشكل أساسي، تراقب علامة التبويب التي يشاركها المقدّم التغييرات على الصفحة وترسل الاختلافات إلى المشاهد باستخدام websocket. عندما يتصفّح المستخدم الصفحة أو يتفاعل معها، ترصد أدوات المراقبة هذه التغييرات وتُبلغ العارض بها باستخدام مكتبة ملخّص التغييرات من Rafael. يساعد ذلك في الحفاظ على الأداء العالي. لا يتم إرسال الصفحة بأكملها لكل لقطة.
كما يشير "رافائيل" في الفيديو، هذا مجرد إثبات جدوى الفكرة. ومع ذلك، أعتقد أنّها طريقة رائعة للجمع بين ميزة جديدة في المنصة، مثل Mutation Observers، وميزة قديمة، مثل Websockets.
الطريقة الثانية: Blob من HTMLDocument + Binary WebSocket
الطريقة التالية هي طريقة خطرت لي مؤخرًا. وهي تشبه نهج Mutation Observers، ولكن بدلاً من إرسال ملخّصات الاختلافات، تنشئ نسخة Blob من HTMLDocument بالكامل وترسلها عبر بروتوكول WebSocket ثنائي. في ما يلي خطوات الإعداد:
- أعِد كتابة جميع عناوين URL على الصفحة لتكون مطلقة. يمنع ذلك الصور الثابتة ومواد عرض CSS من احتواء روابط معطّلة.
- نسخ عنصر مستند الصفحة:
document.documentElement.cloneNode(true); - اجعل النسخة للقراءة فقط وغير قابلة للاختيار، وامنع الانتقال باستخدام CSS
pointer-events: 'none';user-select:'none';overflow:hidden; - سجِّل موضع الانتقال الحالي للصفحة وأضِفه كسمات
data-*في الصفحة المكرّرة. - أنشئ
new Blob()من.outerHTMLالنسخة المكرّرة.
يبدو الرمز على النحو التالي (لقد أجريت تبسيطات من المصدر الكامل):
function screenshotPage() {
// 1. Rewrite current doc's imgs, css, and script URLs to be absolute before
// we duplicate. This ensures no broken links when viewing the duplicate.
urlsToAbsolute(document.images);
urlsToAbsolute(document.querySelectorAll("link[rel='stylesheet']"));
urlsToAbsolute(document.scripts);
// 2. Duplicate entire document tree.
var screenshot = document.documentElement.cloneNode(true);
// 3. Screenshot should be readyonly, no scrolling, and no selections.
screenshot.style.pointerEvents = 'none';
screenshot.style.overflow = 'hidden';
screenshot.style.userSelect = 'none'; // Note: need vendor prefixes
// 4. … read on …
// 5. Create a new .html file from the cloned content.
var blob = new Blob([screenshot.outerHTML], {type: 'text/html'});
// Open a popup to new file by creating a blob URL.
window.open(window.URL.createObjectURL(blob));
}
يحتوي urlsToAbsolute() على تعبيرات نمطية بسيطة لإعادة كتابة عناوين URL النسبية أو التي لا تتضمّن مخططًا إلى عناوين URL كاملة. هذا الإجراء ضروري لكي لا تتعطّل الصور وملفات CSS والخطوط والنصوص البرمجية عند عرضها في سياق عنوان URL لكائن ثنائي كبير (على سبيل المثال، من مصدر مختلف).
كان آخر تعديل أجريته هو إضافة دعم التمرير. عندما ينتقل المقدّم إلى أسفل الصفحة، يجب أن يتابع المشاهد ذلك. لإجراء ذلك، أخزّن الموضعين الحاليين scrollX وscrollY كسمات data-* في العنصر المكرّر HTMLDocument. قبل إنشاء ملف Blob النهائي، يتم إدخال جزء من رمز JS يتم تنفيذه عند تحميل الصفحة:
// 4. Preserve current x,y scroll position of this page. See addOnPageLoad().
screenshot.dataset.scrollX = window.scrollX;
screenshot.dataset.scrollY = window.scrollY;
// 4.5. When screenshot loads (e.g. in blob URL), scroll it to the same location
// of this page. Do this by appending a window.onDOMContentLoaded listener
// which pulls out the screenshot (dupe's) saved scrollX/Y state on the DOM.
var script = document.createElement('script');
script.textContent = '(' + addOnPageLoad_.toString() + ')();'; // self calling.
screenshot.querySelector('body').appendChild(script);
// NOTE: Not to be invoked directly. When the screenshot loads, scroll it
// to the same x,y location of original page.
function addOnPageLoad() {
window.addEventListener('DOMContentLoaded', function(e) {
var scrollX = document.documentElement.dataset.scrollX || 0;
var scrollY = document.documentElement.dataset.scrollY || 0;
window.scrollTo(scrollX, scrollY);
});
إنّ التمرير المزيّف يعطي انطباعًا بأنّنا أخذنا لقطة شاشة لجزء من الصفحة الأصلية، بينما في الواقع، قمنا بنسخ الصفحة بأكملها وأعدنا تحديد موضعها فقط. #clever
عرض توضيحي
ولكن لمشاركة علامة التبويب، نحتاج إلى التقاط علامة التبويب باستمرار وإرسالها إلى المشاهدين. لهذا الغرض، كتبتُ خادمًا صغيرًا لبروتوكول WebSocket في Node وتطبيقًا وتطبيقًا مختصرًا يوضّح سير العمل. إذا لم تكن مهتمًا بالرمز، إليك فيديو قصير عن كيفية عمله:
تحسينات مستقبلية
من بين التحسينات عدم تكرار المستند بأكمله في كل إطار. هذا أمر غير مفيد، وهو ما يفعله مثال Mutation Observer بشكل جيد. ومن التحسينات الأخرى معالجة صور الخلفية النسبية في CSS في urlsToAbsolute(). هذا أمر لا يأخذه
النص الحالي في الاعتبار.
الطريقة 3: واجهة برمجة تطبيقات إضافات Chrome + بروتوكول WebSocket الثنائي
في مؤتمر Google I/O لعام 2012، عرضتُ طريقة أخرى لمشاركة محتوى علامة تبويب متصفّح على الشاشة. ومع ذلك، هذه خدعة. تتطلّب واجهة برمجة تطبيقات لإضافات Chrome، وليس سحر HTML5 خالصًا.
المصدر لهذا المثال متاح أيضًا على Github، ولكن إليك الخلاصة:
- التقاط علامة التبويب الحالية كعنوان URL لبيانات .png تتضمّن إضافات Chrome واجهة برمجة تطبيقات لذلك
chrome.tabs.captureVisibleTab(). - حوِّل dataURL إلى
Blob. يمكنك الاطّلاع على مساعدconvertDataURIToBlob(). - أرسِل كل Blob (إطار) إلى العارض باستخدام websocket ثنائي من خلال ضبط
socket.responseType='blob'.
مثال
إليك رمز برمجي لأخذ لقطة شاشة لعلامة التبويب الحالية كملف png وإرسال الإطار من خلال websocket:
var IMG_MIMETYPE = 'images/jpeg'; // Update to image/webp when crbug.com/112957 is fixed.
var IMG_QUALITY = 80; // [0-100]
var SEND_INTERVAL = 250; // ms
var ws = new WebSocket('ws://…', 'dumby-protocol');
ws.binaryType = 'blob';
function captureAndSendTab() {
var opts = {format: IMG_MIMETYPE, quality: IMG_QUALITY};
chrome.tabs.captureVisibleTab(null, opts, function(dataUrl) {
// captureVisibleTab returns a dataURL. Decode it -> convert to blob -> send.
ws.send(convertDataURIToBlob(dataUrl, IMG_MIMETYPE));
});
}
var intervalId = setInterval(function() {
if (ws.bufferedAmount == 0) {
captureAndSendTab();
}
}, SEND_INTERVAL);
تحسينات مستقبلية
معدّل عرض الإطارات جيد بشكل مدهش في هذه اللعبة، ولكن يمكن أن يكون أفضل. من بين التحسينات التي يمكن إجراؤها إزالة عبء تحويل dataURL إلى Blob. للأسف، لا يوفّر لنا chrome.tabs.captureVisibleTab() سوى dataURL. إذا عرضت Blob أو Typed Array، يمكننا إرسال ذلك مباشرةً من خلال websocket بدلاً من إجراء التحويل إلى Blob بأنفسنا. يُرجى وضع علامة نجمة على crbug.com/32498 لتحقيق ذلك.
الطريقة الرابعة: WebRTC - المستقبل الحقيقي
أخيرًا وليس آخرًا،
سيتم تحقيق مستقبل مشاركة الشاشة في المتصفّح من خلال WebRTC. في 14 أغسطس 2012، اقترح الفريق واجهة برمجة تطبيقات WebRTC Tab Content Capture لمشاركة محتوى علامات التبويب:
إلى أن يصبح هذا الخيار جاهزًا، يمكننا استخدام الطرق من 1 إلى 3.
الخاتمة
بالتالي، يمكن مشاركة علامة تبويب المتصفّح باستخدام تكنولوجيا الويب الحالية.
ولكن… يجب أخذ هذا البيان بحذر. على الرغم من أنّ الأساليب الواردة في هذه المقالة رائعة، إلا أنّها لا توفّر تجربة مستخدم رائعة للمشاركة بطريقة أو بأخرى. سيتغير كل ذلك مع جهود WebRTC Tab Content Capture، ولكن حتى يصبح حقيقة، سنبقى مع المكونات الإضافية للمتصفح أو الحلول المحدودة مثل تلك التي تمت تغطيتها هنا.
هل لديك أساليب أخرى؟ انشر تعليقًا.