Trong vài năm qua, tôi đã giúp một số công ty đạt được chức năng chia sẻ màn hình chỉ bằng cách sử dụng các công nghệ trình duyệt. Theo kinh nghiệm của tôi, việc triển khai VNC chỉ trong các công nghệ nền tảng web (tức là không có trình bổ trợ) là một vấn đề khó khăn. Có rất nhiều điều cần cân nhắc và nhiều thách thức cần vượt qua. Truyền vị trí con trỏ chuột, chuyển tiếp thao tác nhấn phím và vẽ lại màu 24 bit đầy đủ ở tốc độ 60 khung hình/giây chỉ là một số vấn đề.
Ghi lại nội dung thẻ
Nếu chúng ta loại bỏ những phức tạp của tính năng chia sẻ màn hình truyền thống và tập trung vào việc chia sẻ nội dung của một thẻ trình duyệt, thì vấn đề sẽ được đơn giản hoá rất nhiều, cụ thể là: a.) chụp thẻ hiển thị ở trạng thái hiện tại và b.) gửi "khung" đó qua mạng. Về cơ bản, chúng ta cần một cách để chụp nhanh DOM và chia sẻ DOM đó.
Phần chia sẻ rất dễ dàng. Websocket có khả năng gửi dữ liệu ở nhiều định dạng (chuỗi, JSON, tệp nhị phân). Phần chụp nhanh là một vấn đề khó khăn hơn nhiều. Các dự án như html2canvas đã giải quyết vấn đề chụp ảnh màn hình HTML bằng cách triển khai lại công cụ kết xuất của trình duyệt…trong JavaScript! Một ví dụ khác là Google Ý kiến phản hồi, mặc dù ứng dụng này không phải là nguồn mở. Những loại dự án này rất thú vị, nhưng cũng rất chậm. Bạn sẽ may mắn nếu có được tốc độ truyền 1 khung hình/giây, chứ đừng nói đến tốc độ 60 khung hình/giây mà bạn mong muốn.
Bài viết này thảo luận một số giải pháp chứng minh khái niệm mà tôi yêu thích nhất để "chia sẻ màn hình" một thẻ.
Phương thức 1: Trình quan sát đột biến + WebSocket
+Rafael Weinstein đã minh hoạ một phương pháp phản chiếu thẻ vào đầu năm nay. Kỹ thuật của anh sử dụng Trình quan sát đột biến và WebSocket.
Về cơ bản, thẻ mà người trình bày đang chia sẻ sẽ theo dõi các thay đổi đối với trang và gửi các thay đổi cho người xem bằng websocket. Khi người dùng cuộn hoặc tương tác với trang, trình quan sát sẽ nhận thấy những thay đổi này và báo cáo lại cho người xem bằng cách sử dụng thư viện tóm tắt đột biến của Rafael. Điều này giúp duy trì hiệu suất. Toàn bộ trang không được gửi cho mỗi khung.
Như Rafael đã chỉ ra trong video, đây chỉ là một minh chứng cho khái niệm. Tuy nhiên, tôi nghĩ đây là một cách gọn gàng để kết hợp một tính năng nền tảng mới hơn như Trình quan sát đột biến với một tính năng cũ hơn như Websocket.
Phương thức 2: Blob từ HTMLDocument + WebSocket nhị phân
Phương thức tiếp theo này là một phương thức mà tôi mới biết gần đây. Phương thức này tương tự như phương pháp của Trình quan sát đột biến, nhưng thay vì gửi các thay đổi tóm tắt, phương thức này sẽ tạo một bản sao Blob của toàn bộ HTMLDocument
và gửi bản sao đó qua một websocket nhị phân. Dưới đây là cách thiết lập theo từng bước:
- Viết lại tất cả URL trên trang thành URL tuyệt đối. Điều này giúp tài sản CSS và hình ảnh tĩnh không chứa đường liên kết bị hỏng.
- Sao chép phần tử tài liệu của trang:
document.documentElement.cloneNode(true);
- Đặt bản sao ở chế độ chỉ có thể đọc, không thể chọn và ngăn thao tác cuộn bằng CSS
pointer-events: 'none';user-select:'none';overflow:hidden;
- Ghi lại vị trí cuộn hiện tại của trang và thêm các vị trí đó dưới dạng thuộc tính
data-*
trên trang trùng lặp. - Tạo
new Blob()
từ.outerHTML
của bản sao.
Mã sẽ có dạng như sau (tôi đã đơn giản hoá từ nguồn đầy đủ):
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()
chứa các biểu thức chính quy đơn giản để viết lại URL tương đối/không có giao thức thành URL tuyệt đối. Điều này là cần thiết để hình ảnh, css, phông chữ và tập lệnh không bị lỗi khi xem trong ngữ cảnh của URL blob (ví dụ: từ một nguồn gốc khác).
Một điều chỉnh cuối cùng tôi thực hiện là thêm tính năng hỗ trợ cuộn. Khi người trình bày cuộn trang, người xem phải làm theo. Để làm việc đó, tôi lưu trữ các vị trí scrollX
và scrollY
hiện tại dưới dạng thuộc tính data-*
trên HTMLDocument
trùng lặp. Trước khi Blob cuối cùng được tạo, một chút mã JS sẽ được chèn vào và kích hoạt khi tải trang:
// 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);
});
Việc giả mạo thao tác cuộn tạo cảm giác như chúng ta đã chụp ảnh màn hình một phần của trang gốc, trong khi thực tế, chúng ta đã sao chép toàn bộ trang và chỉ định vị lại trang đó. #clever
Bản minh hoạ
Nhưng đối với tính năng chia sẻ thẻ, chúng ta cần liên tục chụp lại thẻ và gửi cho người xem. Để làm được việc đó, tôi đã viết một máy chủ websocket Node, ứng dụng và một bookmarklet nhỏ minh hoạ quy trình. Nếu bạn không quan tâm đến mã, hãy xem video ngắn sau đây về cách hoạt động của các thành phần:
Các điểm cải tiến trong tương lai
Một cách tối ưu hoá là không sao chép toàn bộ tài liệu trên mỗi khung. Đó là một sự lãng phí và là điều mà ví dụ về Mutation Observer làm rất tốt. Một điểm cải tiến khác là xử lý hình nền CSS tương đối trong urlsToAbsolute()
. Đó là điều mà tập lệnh hiện tại không xem xét.
Phương thức 3: API tiện ích Chrome + WebSocket nhị phân
Tại Google I/O 2012, tôi đã minh hoạ một phương pháp khác để chia sẻ nội dung của một thẻ trình duyệt trên màn hình. Tuy nhiên, đây là một cách gian lận. API này yêu cầu API tiện ích Chrome: không phải là HTML5 thuần tuý.
Nguồn của bản dịch này cũng có trên GitHub, nhưng tóm tắt là:
- Ghi lại thẻ hiện tại dưới dạng dataURL .png. Tiện ích của Chrome có một API cho
chrome.tabs.captureVisibleTab()
đó. - Chuyển đổi dataURL thành
Blob
. Xem trình trợ giúpconvertDataURIToBlob()
. - Gửi từng Blob (khung) đến trình xem bằng cách sử dụng websocket nhị phân bằng cách đặt
socket.responseType='blob'
.
Ví dụ:
Dưới đây là mã để chụp ảnh màn hình thẻ hiện tại dưới dạng tệp png và gửi khung đó qua 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);
Các điểm cải tiến trong tương lai
Tốc độ khung hình của video này rất tốt, nhưng vẫn có thể tốt hơn nữa. Một điểm cải tiến là xoá hao tổn khi chuyển đổi dataURL thành Blob. Rất tiếc, chrome.tabs.captureVisibleTab()
chỉ cung cấp cho chúng ta một dataURL. Nếu hàm này trả về một Blob hoặc Mảng đã nhập, chúng ta có thể gửi trực tiếp thông qua websocket thay vì tự chuyển đổi sang Blob. Vui lòng gắn dấu sao vào crbug.com/32498 để thực hiện việc đó!
Phương thức 4: WebRTC – tương lai thực sự
Cuối cùng nhưng không kém phần quan trọng!
WebRTC sẽ hiện thực hoá tương lai của tính năng chia sẻ màn hình trong trình duyệt. Vào ngày 14 tháng 8 năm 2012, nhóm đã đề xuất API WebRTC Tab Content Capture (Chụp nội dung thẻ WebRTC) để chia sẻ nội dung thẻ:
Cho đến khi anh chàng này sẵn sàng, chúng ta sẽ sử dụng các phương thức 1-3.
Kết luận
Vì vậy, bạn có thể chia sẻ thẻ trình duyệt bằng công nghệ web hiện nay!
Nhưng…bạn nên thận trọng với tuyên bố đó. Mặc dù gọn gàng, nhưng các kỹ thuật trong bài viết này vẫn chưa mang lại trải nghiệm người dùng chia sẻ tuyệt vời theo cách này hay cách khác. Tất cả điều đó sẽ thay đổi với nỗ lực Chụp nội dung thẻ WebRTC, nhưng cho đến khi điều đó trở thành hiện thực, chúng ta vẫn còn các trình bổ trợ trình duyệt hoặc các giải pháp hạn chế như những giải pháp được đề cập ở đây.
Bạn có thêm kỹ thuật nào không? Đăng bình luận!