HTML5'te bir tarayıcı sekmesini ekran paylaşımı

Son birkaç yıldır birkaç farklı şirketin yalnızca tarayıcı teknolojilerini kullanarak ekran paylaşımına benzer işlevler elde etmesine yardımcı oldum. Deneyimlerimize göre, VNC'yi yalnızca web platformu teknolojilerinde (ör. eklenti yok) uygulamak zor bir sorundur. Göz önünde bulundurulması gereken birçok şey ve üstesinden gelinmesi gereken birçok zorluk vardır. Fare işaretçisinin konumunu aktarma, tuş vuruşlarını yönlendirme ve 60 fps'de tam 24 bit renk yeniden boyamaya ulaşma, bu sorunlardan sadece birkaçıdır.

Sekme içeriğini yakalama

Geleneksel ekran paylaşımının karmaşıklığını ortadan kaldırıp bir tarayıcı sekmesinin içeriğini paylaşmaya odaklanırsak sorun büyük ölçüde basitleşir. Artık tek yapmanız gereken a.) görünen sekmeyi mevcut durumunda yakalamak ve b.) bu "kareyi" kablo üzerinden göndermektir. Temel olarak, DOM'un anlık görüntüsünü alıp paylaşabileceğimiz bir yönteme ihtiyacımız var.

Paylaşım kısmı kolaydır. Web soketleri, verileri farklı biçimlerde (dize, JSON, ikili) gönderme konusunda çok yeteneklidir. Anlık görüntü alma kısmı çok daha zor bir sorundur. html2canvas gibi projeler, tarayıcının oluşturma motorunu JavaScript'te yeniden uygulayarak HTML ekran görüntülerini yakalama sorununu çözdü. Açık kaynak olmasa da Google Geri Bildirim de buna örnek gösterilebilir. Bu tür projeler çok havalı olsa da korkunç derecede yavaştır. 1 fps'lik bir aktarım hızı elde etmeniz bile şans eseri olur. Hele 60 fps'den bahsetmeye bile gerek yok.

Bu makalede, sekme "ekran paylaşımı" için en sevdiğim kavram kanıtı çözümlerinden birkaçı ele alınmaktadır.

1. Yöntem: Mutasyon Gözlemcileri + WebSocket

Sekme yansıtma yaklaşımlarından biri bu yılın başlarında +Rafael Weinstein tarafından gösterilmişti. Bu teknikte MutationObserver'lar ve bir WebSocket kullanılır.

Temel olarak, sunucunun paylaştığı sekme, sayfadaki değişiklikleri izler ve bir web soketi kullanarak izleyiciye fark bilgilerini gönderir. Kullanıcı sayfayı kaydırırken veya sayfayla etkileşim kurarken gözlemciler bu değişiklikleri algılar ve Rafael'in mutasyon özeti kitaplığını kullanarak izleyiciye bildirir. Bu sayede performansı koruyabilirsiniz. Her kare için sayfanın tamamı gönderilmez.

Rafael'in videoda belirttiği gibi, bu yalnızca bir kavram kanıtıdır. Yine de Mutation Observers gibi daha yeni bir platform özelliğini Websockets gibi daha eski bir özellikle birleştirmenin iyi bir yöntem olduğunu düşünüyorum.

2. Yöntem: HTMLDocument'den Blob + İkili WebSocket

Bir sonraki yöntem, yakın zamanda aklıma gelen bir yöntem. Bu yaklaşım, Mutasyon Gözlemci yaklaşımına benzer ancak özet farklar göndermek yerine HTMLDocument'ün tamamının Blob kopyasını oluşturur ve bunu ikili bir web soketi üzerinden gönderir. Kurulumlara göre ayarlar aşağıda verilmiştir:

  1. Sayfadaki tüm URL'leri mutlak olacak şekilde yeniden yazın. Bu işlem, statik resim ve CSS öğelerinin bozuk bağlantılar içermesini önler.
  2. Sayfanın belge öğesini klonlayın: document.documentElement.cloneNode(true);
  3. CSS kullanarak klonu salt okunur ve seçilemez hale getirin ve kaydırmayı önleyin pointer-events: 'none';user-select:'none';overflow:hidden;
  4. Sayfanın mevcut kaydırma konumunu yakalayıp kopyada data-* özellikleri olarak ekleyin.
  5. Kopyanın .outerHTML öğesinden bir new Blob() oluşturun.

Kod şu şekilde görünür (Tam kaynaktan basitleştirmeler yaptım):

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(), göreli/şemasız URL'leri mutlak URL'ler olarak yeniden yazmak için basit normal ifade içerir. Bu, resimlerin, CSS'nin, yazı tiplerinin ve komut dosyalarının bir blob URL'si bağlamında (ör. farklı bir kaynaktan) görüntülenirken bozulmaması için gereklidir.

Son olarak, kaydırma desteği ekledim. Sunucu sayfayı kaydırdığında izleyici de sayfayı takip etmelidir. Bunu yapmak için mevcut scrollX ve scrollY konumlarını kopya HTMLDocument öğesinde data-* özellikleri olarak saklıyorum. Son Blob oluşturulmadan önce, sayfa yüklendiğinde tetiklenen bir JS parçası eklenir:

// 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);
    });

Kaydırma işlemini taklit etmek, orijinal sayfanın bir bölümünün ekran görüntüsünü aldığımız izlenimini verir. Ancak aslında sayfanın tamamını kopyalayıp yalnızca yeniden konumlandırmış oluruz. #clever

Demo

Ancak sekme paylaşımı için sekmeyi sürekli olarak yakalayıp izleyicilere göndermemiz gerekir. Bunun için akışı gösteren küçük bir Node websocket sunucusu, uygulaması ve yer işareti yazdım. Kod ile ilgilenmiyorsanız bu kısa videoda işlevlerin nasıl kullanıldığını görebilirsiniz:

Gelecekteki İyileştirmeler

Bir optimizasyon, dokümanın tamamını her karede kopyalamamaktır. Bu, Mutation Observer örneğinin iyi yaptığı bir şeydir. urlsToAbsolute()'te göreli CSS arka plan resimlerinin işlenmesi de bir diğer iyileştirmedir. Mevcut komut dosyası bunu dikkate almıyor.

3. yöntem: Chrome Extension API + İkili WebSocket

Google I/O 2012'de, tarayıcı sekmesinin içeriğini ekran paylaşımı için başka bir yaklaşımı gösterdim. Ancak bu bir hile. Saf HTML5 sihiriyle değil, Chrome Uzantısı API'si ile çalışır.

Bu kaynak da GitHub'da mevcuttur ancak özeti şudur:

  1. Geçerli sekmeyi .png dataURL olarak yakalar. Chrome uzantılarında bu chrome.tabs.captureVisibleTab() için bir API bulunur.
  2. dataURL'yi Blob olarak dönüştürün. convertDataURIToBlob() yardımcısına bakın.
  3. socket.responseType='blob' ayarını yaparak her Blob'ı (kare) izleyiciye ikili bir web soketi kullanarak gönderin.

Örnek

Mevcut sekmenin ekran görüntüsünü PNG olarak alıp kareyi bir web soketi üzerinden gönderen kodu burada bulabilirsiniz:

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);

Gelecekteki İyileştirmeler

Bu videoda kare hızı şaşırtıcı derecede iyi ancak daha da iyi olabilir. Bir iyileştirme, dataURL'yi Blob'a dönüştürmenin ek yükünü kaldırmak olabilir. Maalesef chrome.tabs.captureVisibleTab() bize yalnızca bir dataURL veriyor. Bir Blob veya Typed Array döndürülürse Blob'a dönüşümü kendimiz yapmak yerine doğrudan websocket üzerinden gönderebiliriz. Bunun gerçekleşmesi için lütfen crbug.com/32498 adresine yıldız ekleyin.

4. yöntem: WebRTC - gerçek gelecek

Son olarak,

Tarayıcıda ekran paylaşımının geleceği WebRTC ile şekillenecek. Ekip, 14 Ağustos 2012'de sekme içeriklerini paylaşmak için WebRTC Sekme İçeriği Yakalama API'sini önerdi:

Bu adam hazır olana kadar 1-3 yöntemlerini kullanabiliriz.

Sonuç

Bu nedenle, günümüzün web teknolojisiyle tarayıcı sekmesi paylaşımı mümkün.

Ancak bu ifadeyi dikkate almanız gerekir. Bu makaledeki teknikler kullanışlı olsa da bir şekilde mükemmel bir paylaşım kullanıcı deneyimi sunmuyor. Tüm bunlar WebRTC Sekme İçeriği Yakalama özelliğiyle değişecek. Ancak bu özellik kullanıma sunulana kadar tarayıcı eklentileri veya burada açıklanan sınırlı çözümlerle yetinmemiz gerekecek.

Başka teknikler var mı? Yorum yazın.