בשנתיים האחרונות עזרתי לכמה חברות שונות להשיג פונקציונליות דמוית שיתוף מסך באמצעות טכנולוגיות דפדפן בלבד. מניסיוני, הטמעת VNC רק בטכנולוגיות של פלטפורמות אינטרנט (כלומר, ללא פלאגינים) היא בעיה קשה. יש הרבה דברים שצריך לקחת בחשבון והרבה אתגרים שצריך להתגבר עליהם. העברת המיקום של סמן העכבר, העברת הקשות על מקשים וצביעה מחדש של צבעים מלאים של 24 ביט בקצב של 60 פריימים לשנייה הן רק חלק מהבעיות.
הקלטת התוכן של הכרטיסייה
אם נסיר את המורכבות של שיתוף המסך המסורתי ונתמקד בשיתוף התוכן של כרטיסייה בדפדפן, הבעיה תהיה פשוטה בהרבה: א) תיעוד הכרטיסייה הגלויה במצב הנוכחי שלה, ו-ב) שליחת ה "פריים" הזה דרך החיבור. בעיקרון, אנחנו צריכים דרך ליצור תמונת מצב של ה-DOM ולשתף אותה.
החלק של השיתוף הוא פשוט. אפשר לשלוח נתונים ב-WebSockets בפורמטים שונים (מחרוזת, JSON, בינארי). החלק של יצירת תמונת המצב הוא בעיה הרבה יותר קשה. בפרויקטים כמו html2canvas ניסו לפתור את הבעיה של צילום מסך ב-HTML על ידי הטמעה מחדש של מנוע הרינדור של הדפדפן… ב-JavaScript! דוגמה נוספת היא משוב Google, למרות שהוא לא בקוד פתוח. הפרויקטים האלה מגניבים מאוד, אבל הם גם איטיים להחריד. אם תצליחו להגיע לקצב של פריים אחד לשנייה, זה יהיה נס. בטח שלא תגיעו לקצב הנחשק של 60 פריימים לשנייה.
במאמר הזה נדון בכמה מהפתרונות המועדפים עליי להוכחת היתכנות של "שיתוף מסך" של כרטיסייה.
שיטה 1: Mutation Observers + WebSocket
גישה אחת לשיקוף כרטיסייה הוצגה על ידי +Rafael Weinstein מוקדם יותר השנה. השיטה שלו משתמשת ב-Mutation Observers וב-WebSocket.
בעצם, הכרטיסייה שהמציג משתף עוקבת אחרי שינויים בדף ושולחת את ההבדלים לצופה באמצעות websocket. כשהמשתמש גולל בדף או יוצר איתו אינטראקציה, הצופים מזהים את השינויים האלה ומדווחים עליהם בחזרה לצופה באמצעות ספריית סיכום המוטציות של רפאל. כך הביצועים נשארים טובים. הדף כולו לא נשלח בכל פריים.
כמו שרפאל מציין בסרטון, זו רק הוכחת היתכנות. עם זאת, אני חושב שזו דרך נחמדה לשלב תכונה חדשה יותר של הפלטפורמה כמו Mutation Observers עם תכונה ישנה יותר כמו Websockets.
שיטה 2: Blob מ-HTMLDocument + 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 של blob (למשל, ממקור אחר).
השינוי האחרון שעשיתי היה להוסיף תמיכה בגלילה. כשהמציג גולל את הדף, הצופה אמור לעקוב אחריו. כדי לעשות את זה, אני שומר את המיקומים הנוכחיים של 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 Extension API + Binary WebSocket
ב-Google I/O 2012, הדגמתי גישה אחרת לשיתוף מסך של תוכן בכרטיסייה בדפדפן. אבל זה לא נכון. הוא דורש API של תוסף ל-Chrome, ולא רק HTML5.
קוד המקור של הדוגמה הזו זמין גם ב-GitHub, אבל העיקר הוא:
- צילום הכרטיסייה הנוכחית כ-dataURL בפורמט .png. לתוספים ל-Chrome יש API בשביל זה
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 או מערך מוקלד, היינו יכולים לשלוח אותו ישירות דרך ה-WebSocket במקום לבצע את ההמרה ל-Blob בעצמנו. כדי שזה יקרה, צריך לסמן בכוכב את crbug.com/32498.
שיטה 4: WebRTC – העתיד האמיתי
ואחרון חביב!
העתיד של שיתוף המסך בדפדפן יתממש באמצעות WebRTC. ב-14 באוגוסט 2012, הצוות הציע את ה-API WebRTC Tab Content Capture לשיתוף תוכן של כרטיסיות:
עד שהשיטה הזו תהיה מוכנה, נשארו לנו שיטות 1 עד 3.
סיכום
אז אפשר לשתף כרטיסייה בדפדפן באמצעות טכנולוגיית האינטרנט של היום.
אבל… צריך לקחת את האמירה הזו בעירבון מוגבל. השיטות שמוצגות במאמר הזה הן נחמדות, אבל הן לא מספקות חוויית משתמש מעולה בשיתוף. כל זה ישתנה עם המאמץ של WebRTC Tab Content Capture, אבל עד שזה יהפוך למציאות, אנחנו נשארים עם יישומי פלאגין לדפדפן או פתרונות מוגבלים כמו אלה שנסקרים כאן.
יש לכם עוד שיטות? פרסום תגובה