การแชร์แท็บเบราว์เซอร์ใน HTML5 ใช่ไหม

ในช่วง 2-3 ปีที่ผ่านมา เราได้ช่วยให้บริษัทหลายแห่งบรรลุฟังก์ชันการทำงานที่คล้ายกับการแชร์หน้าจอโดยใช้เทคโนโลยีเบราว์เซอร์เท่านั้น จากประสบการณ์ของเรา การใช้ VNC ในเทคโนโลยีแพลตฟอร์มเว็บเพียงอย่างเดียว (ไม่มีปลั๊กอิน) เป็นปัญหาที่แก้ยาก มีหลายสิ่งที่ต้องพิจารณาและอุปสรรคมากมายที่ต้องเอาชนะ การส่งต่อตำแหน่งเคอร์เซอร์ของเมาส์ การส่งต่อการกดแป้นพิมพ์ และการทาสีใหม่ด้วยสี 24 บิตที่ 60 fps เป็นเพียงปัญหาบางส่วนเท่านั้น

การจับภาพเนื้อหาแท็บ

หากเรานำความซับซ้อนของการแชร์หน้าจอแบบดั้งเดิมออกและมุ่งเน้นที่การแชร์เนื้อหาของแท็บเบราว์เซอร์ ปัญหาจะง่ายขึ้นมาก โดยแบ่งออกเป็น 2 ส่วน ได้แก่ ก) การจับภาพแท็บที่มองเห็นได้ในสถานะปัจจุบัน และ ข) การส่ง "เฟรม" นั้นผ่านเครือข่าย โดยพื้นฐานแล้ว เราต้องการวิธีจับภาพ DOM และแชร์

ส่วนการแชร์นั้นง่ายมาก WebSocket สามารถส่งข้อมูลในรูปแบบต่างๆ (สตริง, JSON, ไบนารี) ส่วนการจับภาพเป็นปัญหาที่ยากกว่ามาก โปรเจ็กต์อย่าง html2canvas ได้แก้ปัญหาการจับภาพหน้าจอ HTML ด้วยการนำเครื่องมือแสดงผลของเบราว์เซอร์กลับมาใช้ใหม่…ใน JavaScript อีกตัวอย่างหนึ่งคือ Google Feedback แต่ไม่ใช่โอเพนซอร์ส โปรเจ็กต์ประเภทนี้เจ๋งมาก แต่ก็ช้ามากเช่นกัน คุณคงโชคดีแล้วหากได้อัตราข้อมูล 1 fps ยิ่งไม่ต้องพูดถึง 60 fps ที่ทุกคนต้องการ

บทความนี้กล่าวถึงโซลูชันการพิสูจน์แนวคิดที่ฉันชอบ 2-3 รายการสําหรับ "การแชร์หน้าจอ" แท็บ

วิธีที่ 1: Mutation Observer + WebSocket

+Rafael Weinstein ได้สาธิตวิธีมิเรอร์แท็บไปเมื่อต้นปีนี้ เทคนิคของเขาใช้ Mutation Observer และ WebSocket

โดยพื้นฐานแล้ว แท็บที่ผู้นำเสนอแชร์จะคอยตรวจสอบการเปลี่ยนแปลงในหน้าเว็บและส่งความแตกต่างไปยังผู้ดูโดยใช้ WebSocket เมื่อผู้ใช้เลื่อนหรือโต้ตอบกับหน้าเว็บ ผู้สังเกตการณ์จะจับการเปลี่ยนแปลงเหล่านี้และรายงานกลับไปให้ผู้ดูทราบโดยใช้คลังสรุปข้อมูลการกลายพันธุ์ของ Rafael วิธีนี้จะช่วยให้ระบบมีประสิทธิภาพ ระบบจะไม่ส่งทั้งหน้าสำหรับทุกเฟรม

ดังที่ Rafael ชี้ให้เห็นในวิดีโอ นี่เป็นเพียงแค่การพิสูจน์แนวคิด อย่างไรก็ตาม เราคิดว่านี่ก็เป็นวิธีที่ดีในการรวมฟีเจอร์แพลตฟอร์มใหม่อย่าง Mutation Observer เข้ากับฟีเจอร์เก่าอย่าง Websockets

วิธีที่ 2: Blob จาก HTMLDocument + WebSocket แบบไบนารี

วิธีถัดไปนี้เพิ่งเพิ่งค้นพบเมื่อเร็วๆ นี้ ซึ่งคล้ายกับแนวทางของ Mutation Observers แต่แทนที่จะส่งความแตกต่างของข้อมูลสรุป ระบบจะสร้าง Blob ที่โคลนจาก HTMLDocument ทั้งหมดและส่งผ่าน WebSocket แบบไบนารี การตั้งค่าตามการตั้งค่ามีดังนี้

  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 สัมพัทธ์/ไม่มีรูปแบบใหม่เป็น 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 + WebSocket แบบไบนารี

ในงาน Google I/O 2012 เราได้สาธิตวิธีอื่นในการแชร์หน้าจอเนื้อหาของแท็บเบราว์เซอร์ แต่อันนี้เป็นการโกง ต้องใช้ Chrome Extension API ไม่ใช่ 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);

การปรับปรุงในอนาคต

อัตราเฟรมของวิดีโอนี้ดีมากอย่างน่าประหลาดใจ แต่ก็อาจทำได้ดีกว่านี้ การปรับปรุงอย่างหนึ่งคือการลดค่าใช้จ่ายในการแปลง dataURL เป็น Blob ขออภัย chrome.tabs.captureVisibleTab() มีให้เราเพียง dataURL หากผลลัพธ์เป็น Blob หรืออาร์เรย์ที่กําหนดประเภทแล้ว เราจะส่งผ่าน WebSocket ได้โดยตรงแทนที่จะแปลงเป็น Blob ด้วยตนเอง โปรดติดดาว crbug.com/32498 เพื่อให้เราดำเนินการดังกล่าว

วิธีที่ 4: WebRTC - อนาคตที่แท้จริง

สุดท้ายแต่ไม่ท้ายสุด

อนาคตของการแชร์หน้าจอในเบราว์เซอร์จะกลายเป็นจริงด้วย WebRTC ในวันที่ 14 สิงหาคม 2012 ทีมได้เสนอ API การจับภาพเนื้อหาแท็บ WebRTC สำหรับการแชร์เนื้อหาแท็บ

ในระหว่างนี้ เรามีวิธี 1-3 ให้เลือก

บทสรุป

ดังนั้นการแชร์แท็บเบราว์เซอร์จึงเป็นไปได้ด้วยเทคโนโลยีเว็บในปัจจุบัน

แต่…คุณควรพิจารณาข้อความนี้อย่างถี่ถ้วน แม้ว่าจะดูดี แต่เทคนิคในบทความนี้ยังขาด UX การแชร์ที่ยอดเยี่ยมในบางแง่มุม ทุกอย่างจะเปลี่ยนไปเมื่อเราใช้ WebRTC Tab Content Capture แต่ในระหว่างนี้ เราต้องใช้ปลั๊กอินของเบราว์เซอร์หรือโซลูชันแบบจำกัดดังที่อธิบายไว้ที่นี่

หากมีเทคนิคเพิ่มเติม โพสต์ความคิดเห็น