Au cours des deux dernières années, j'ai aidé plusieurs entreprises à mettre en place des fonctionnalités de partage d'écran à l'aide de technologies de navigateur uniquement. D'après mon expérience, implémenter VNC uniquement dans les technologies de plate-forme Web (c'est-à-dire sans plug-in) est un problème difficile. De nombreux éléments doivent être pris en compte et de nombreux défis doivent être relevés. Le relais de la position du pointeur de la souris, la transmission des frappes et la réalisation de recolorations complètes en 24 bits à 60 FPS ne sont que quelques-uns des problèmes.
Capturer le contenu de l'onglet
Si nous supprimons la complexité du partage d'écran traditionnel et nous concentrons sur le partage du contenu d'un onglet de navigateur, le problème se simplifie considérablement : a.) capturer l'onglet visible dans son état actuel et b.) envoyer ce "cadre" via le fil. En gros, nous avons besoin d'un moyen de prendre une photo du DOM et de le partager.
Le partage est simple. Les Websockets sont très efficaces pour envoyer des données dans différents formats (chaîne, JSON, binaire). La partie de création d'instantanés est un problème beaucoup plus difficile. Des projets comme html2canvas ont abordé la capture d'écran HTML en réimplémentant le moteur de rendu du navigateur… en JavaScript. Google Feedback est un autre exemple, bien qu'il ne soit pas Open Source. Ces types de projets sont très intéressants, mais ils sont aussi terriblement lents. Vous aurez de la chance d'obtenir un débit de 1 fps, et encore moins les 60 fps tant convoités.
Cet article présente quelques-unes de mes solutions de preuve de concept préférées pour "partager l'écran" d'un onglet.
Méthode 1: Observateurs de mutation + WebSocket
+Rafael Weinstein a présenté une approche permettant de dupliquer un onglet plus tôt cette année. Sa technique utilise des observateurs de modifications et un WebSocket.
En substance, l'onglet que le présentateur partage surveille les modifications apportées à la page et envoie les différences au lecteur à l'aide d'un websocket. Lorsque l'utilisateur fait défiler la page ou interagit avec elle, les observateurs détectent ces modifications et les transmettent au lecteur à l'aide de la bibliothèque de résumés de mutations de Rafael. Cela permet de maintenir les performances. La page entière n'est pas envoyée pour chaque frame.
Comme Rafael le souligne dans la vidéo, il ne s'agit que d'une preuve de concept. Je pense toutefois que c'est un moyen pratique de combiner une fonctionnalité de plate-forme plus récente, comme les observateurs de mutations, avec une fonctionnalité plus ancienne, comme les Websockets.
Méthode 2: Blob à partir d'un HTMLDocument et WebSocket binaire
Cette méthode m'est apparue récemment. Il est semblable à l'approche des observateurs de mutations, mais au lieu d'envoyer des différences récapitulatives, il crée un clone de blob de l'ensemble de HTMLDocument
et l'envoie via un websocket binaire. Voici la configuration par configuration:
- Réécrivez toutes les URL de la page pour qu'elles soient absolues. Cela évite que les composants Image et CSS statiques contiennent des liens non fonctionnels.
- Clonez l'élément de document de la page:
document.documentElement.cloneNode(true);
. - Rendre le clone en lecture seule, non sélectionnable et empêcher le défilement à l'aide de CSS
pointer-events: 'none';user-select:'none';overflow:hidden;
- Capturez la position de défilement actuelle de la page et ajoutez-la en tant qu'attribut
data-*
sur la copie. - Créez un
new Blob()
à partir du.outerHTML
du double.
Le code se présente comme suit (j'ai simplifié la source complète):
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()
contient des expressions régulières simples pour réécrire les URL relatives/sans schéma en URL absolues. Cela est nécessaire pour que les images, les fichiers CSS, les polices et les scripts ne soient pas endommagés lorsqu'ils sont consultés dans le contexte d'une URL de blob (par exemple, à partir d'une autre origine).
J'ai également ajouté la possibilité de faire défiler l'écran. Lorsque le présentateur fait défiler la page, le spectateur doit le suivre. Pour ce faire, je stocke les positions scrollX
et scrollY
actuelles en tant qu'attributs data-*
sur le HTMLDocument
dupliqué. Avant la création du blob final, un peu de code JavaScript est injecté et se déclenche lors du chargement de la page:
// 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);
});
Simuler le défilement donne l'impression que nous avons capturé une partie de la page d'origine, alors qu'en réalité, nous l'avons dupliquée dans son intégralité et simplement repositionnée. #clever
Démo
Toutefois, pour partager un onglet, nous devons le capturer en continu et l'envoyer aux spectateurs. Pour ce faire, j'ai écrit un petit serveur websocket Node, une application et un bookmarklet qui illustrent le flux. Si le code ne vous intéresse pas, voici une courte vidéo qui vous montre comment procéder:
Améliorations futures
Une optimisation consiste à ne pas dupliquer l'intégralité du document dans chaque frame. Ce gaspillage est évité par l'exemple Mutation Observer. Une autre amélioration consiste à gérer les images de fond CSS relatives dans urlsToAbsolute()
. C'est quelque chose que le script actuel ne prend pas en compte.
Méthode 3: API d'extension Chrome + WebSocket binaire
Lors de la conférence Google I/O 2012, j'ai présenté une autre approche pour partager l'écran du contenu d'un onglet de navigateur. Cependant, il s'agit d'une astuce. Il nécessite une API d'extension Chrome, et non une magie HTML5 pure.
Le code source de celui-ci est également disponible sur GitHub, mais voici l'essentiel:
- Capturez l'onglet actif en tant que dataURL .png. Les extensions Chrome disposent d'une API pour cela :
chrome.tabs.captureVisibleTab()
. - Convertissez la valeur dataURL en
Blob
. Consultez l'aideconvertDataURIToBlob()
. - Envoyez chaque Blob (cadre) au lecteur à l'aide d'un websocket binaire en définissant
socket.responseType='blob'
.
Exemple
Voici du code permettant de créer une capture d'écran de l'onglet actuel au format PNG et d'envoyer le frame via un 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);
Améliorations futures
Le framerate est étonnamment bon pour ce jeu, mais il pourrait être encore meilleur. Une amélioration consisterait à supprimer les frais généraux liés à la conversion du dataURL en blob. Malheureusement, chrome.tabs.captureVisibleTab()
ne nous fournit qu'un dataURL. S'il a renvoyé un Blob ou un tableau typé, nous pouvons l'envoyer directement via le websocket au lieu de convertir nous-mêmes le Blob. Pour ce faire, veuillez ajouter crbug.com/32498 à vos favoris.
Méthode 4: WebRTC, l'avenir
Enfin,
L'avenir du partage d'écran dans le navigateur sera assuré par WebRTC. Le 14 août 2012, l'équipe a proposé une API WebRTC Tab Content Capture (Capture du contenu des onglets WebRTC) pour partager le contenu des onglets:
En attendant, nous n'avons que les méthodes 1 à 3.
Conclusion
Le partage d'onglets de navigateur est donc possible avec la technologie Web actuelle.
Mais… cette déclaration doit être prise avec des pincettes. Bien que pratiques, les techniques décrites dans cet article ne permettent pas d'offrir une expérience de partage optimale, d'une manière ou d'une autre. Tout cela va changer avec la capture du contenu des onglets WebRTC, mais tant que ce n'est pas une réalité, nous n'avons d'autre choix que de nous contenter de plug-ins de navigateur ou de solutions limitées comme celles décrites ici.
Vous avez d'autres techniques ? Publiez un commentaire !