לשתף את המסך של כרטיסיית דפדפן ב-HTML5?

בשנים האחרונות עזרתי לכמה חברות שונות להשיג פונקציונליות כמו של שיתוף מסך באמצעות טכנולוגיות דפדפנים בלבד. מניסיוני, הטמעת VNC אך ורק בטכנולוגיות של פלטפורמת אינטרנט (כלומר, ללא יישומי פלאגין) היא בעיה קשה. יש הרבה דברים שצריך לקחת בחשבון והמון אתגרים שצריך להתגבר עליהם. העברה של מיקום מצביע העכבר, העברה של הקשות מקשים והשגת צביעה מחדש של צבעי 24-ביט מלאים ב-60fps הן רק כמה מהבעיות.

תיעוד תוכן הכרטיסיות

אם נסיר את המורכבות של שיתוף המסך ונתמקד בשיתוף התוכן של כרטיסייה בדפדפן, הבעיה תהפוך לפשוטה יותר: א.) לכידת הכרטיסייה הגלויה במצבה הנוכחי וב.) שליחת ה "מסגרת" לאורך הכבל. בעיקרון, אנחנו צריכים דרך ליצור תמונת מצב של ה-DOM ולשתף אותו.

החלק של השיתוף קל. Websockets יכולים לשלוח נתונים בפורמטים שונים (מחרוזת, JSON, בינארי). החלק של צילום המסך הוא בעיה קשה יותר. פרויקטים כמו html2canvas נתקלו בניסיונות צילום מסך של HTML על ידי הטמעה מחדש של מנוע העיבוד של הדפדפן... ב-JavaScript! דוגמה נוספת היא משוב Google, למרות שלא מדובר בקוד פתוח. סוגי הפרויקטים האלה מאוד מגניבים, אבל הם גם איטיים להדהים. אולי תזכו לקבל קצב של 1fps, הרבה פחות מהקצב הנחשק של 60fps.

מאמר זה דן בכמה מהפתרונות המועדפים עליי להוכחת היתכנות עבור כרטיסייה של "שיתוף מסך".

שיטה 1: מדדי מוטציה + WebSocket

אחת הדרכים לשיקוף כרטיסייה הודגמה על ידי +רפאל ויינשטיין מוקדם יותר השנה. השיטה שלו משתמשת ב-Mutation Watchers וב-WebSocket.

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

כפי שרפאל מציין בסרטון, זו רק הוכחת היתכנות. עדיין, לדעתי זו דרך נהדרת לשלב תכונת פלטפורמה חדשה יותר כמו Mutation Writeers עם תכונה ישנה יותר כמו Websockets.

שיטה 2: blob מ-HTMLDocument + Binary WebSocket

זו השיטה הבאה שגיליתי לאחרונה. היא דומה לגישה של 'צופים במוטציה', אבל במקום לשלוח הפרשי סיכום, הוא יוצר שכפול של Blob של כל HTMLDocument ושולח אותו דרך שקע אינטרנט בינארי. כך מתבצעת ההגדרה:

  1. משכתבים את כל כתובות ה-URL בדף כך שיהיו מוחלטות. כך, נכסי תמונות סטטיות ונכסי CSS לא יכולים להכיל קישורים מנותקים.
  2. שכפול רכיב המסמך של הדף: document.documentElement.cloneNode(true);
  3. הגדרת השכפול לקריאה בלבד, שלא ניתן לבחור בו, ומניעת גלילה באמצעות CSS pointer-events: 'none';user-select:'none';overflow:hidden;
  4. מצלמים את מיקום הגלילה הנוכחי בדף ומוסיפים אותו כמאפייני data-* בעותק הכפול.
  5. יצירת 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 יחסיות/חסרות סכמה לכתובות מוחלטות. הפעולה הזו הכרחית כדי שתמונות, 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 Writeer עושה עבודה טובה. שיפור נוסף הוא הטיפול בתמונות רקע יחסיות של CSS ב-urlsToAbsolute(). זה משהו שהסקריפט הנוכחי לא לוקח בחשבון.

שיטה 3: Chrome Extension API + Binary WebSocket

ב-Google I/O 2012 הדגמתי גישה נוספת לשיתוף מסך של תוכן בכרטיסייה בדפדפן. עם זאת, מדובר בהונאה. הוא מחייב ממשק API של תוסף Chrome: לא קסם של HTML5 טהור.

המקור של הקוד הזה מופיע גם ב-GitHub, אבל עיקרון הדברים הוא:

  1. יש לתעד את הכרטיסייה הנוכחית ככתובת URL של נתונים מסוג .png לתוספים ל-Chrome יש API בשביל chrome.tabs.captureVisibleTab().
  2. צריך להמיר את ה-dataURL ל-Blob. לעיון בכלי העזר של convertDataURIToBlob().
  3. שולחים כל 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);

שיפורים עתידיים

קצב הפריימים מפתיע לטובה בדוגמה הזו, אבל הוא יכול להיות אפילו טוב יותר. אחד מהשיפורים יהיה הסרת התקורה של המרת כתובת ה-URL של הנתונים ל-Blob. לצערנו, chrome.tabs.captureVisibleTab() מספק לנו רק כתובת URL של נתונים. אם הוא מחזיר Blob או Typed Array, נוכל לשלוח אותם ישירות דרך ה-websocket במקום לבצע את ההמרה ל-Blob בעצמנו. כדי שזה יקרה, צריך לסמן בכוכב את crbug.com/32498.

שיטה 4: WebRTC - העתיד האמיתי

אחרון חביב!

העתיד של שיתוף המסך בדפדפן ימומש על ידי WebRTC. ב-14 באוגוסט 2012 הצוות הציע ממשק API של תיעוד תוכן של כרטיסיות WebRTC לשיתוף תוכן של כרטיסיות:

עד שהבחור הזה יהיה מוכן, נישאר עם שיטות 1-3.

סיכום

לכן, ניתן לשתף כרטיסיות בדפדפן עם טכנולוגיית האינטרנט של ימינו!

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

משתמשים בשיטות נוספות? פרסם תגובה!