Einführung
In diesem Artikel haben wir vermutet, dass wir über die Screen Capture-API sprechen werden. Diese API wurde 2014 geboren und ist schwer als neu zu bezeichnen, aber die Browserunterstützung ist immer noch recht schwach. Trotzdem kann es für persönliche Projekte verwendet werden oder wenn diese Unterstützung nicht so wichtig ist.
Ein paar Links, um Ihnen den Einstieg zu erleichtern:
Falls die Verbindung zur Demo unterbrochen wird (oder wenn Sie zu faul sind, um dorthin zu gelangen), sieht die fertige Demo folgendermaßen aus:

Fangen wir an.
Motivation
Kürzlich kam mir die Idee einer Webanwendung, die in ihrer Arbeit QR-Codes verwendet. Und obwohl sie normalerweise praktisch sind, um beispielsweise lange Links in der realen Welt zu übertragen, auf die Sie das Telefon richten können, ist dies auf dem Desktop etwas komplizierter. Wenn sich der QR-Code auf dem Bildschirm des gleichen Geräts befindet, auf dem Sie ihn lesen müssen, müssen Sie sich mit den Diensten zur Erkennung oder Erkennung vom Telefon herum anlegen und die Daten zurück auf den PC übertragen. Unbequem.
Einige Produkte wie 1Password bieten eine interessante Lösung für diese Situation. Wenn Sie ein Konto aus einem QR-Code einrichten müssen, wird ein durchscheinendes Fenster geöffnet, das Sie mit dem Code über das Bild ziehen können. Es wird automatisch erkannt. So sieht es aus:

Es wäre ideal, wenn wir etwas Ähnliches für unsere Anwendung implementieren könnten. Aber wahrscheinlich funktioniert es im Browser nicht ...
Na ja, fast. Hier getDisplayMedia
die Screen Capture-API mit ihrer einzigen getDisplayMedia
Methode. getDisplayMedia
ist wie getUserMedia
, nur für den getUserMedia
anstelle der Kamera. Leider ist die Browser-Unterstützung, wie oben erwähnt, bei weitem nicht so weit verbreitet wie der Zugriff auf die Kamera. Laut MDN kann es in Firefox, Chrome, Edge verwendet werden (obwohl es sich dort an der falschen Stelle befindet - direkt im navigator
und nicht in navigator.mediaDevices
) + Edge Mobile und ... Opera für Android.
Eine ziemlich merkwürdige Auswahl an mobilen Browsern neben den erwarteten Big Two.
Die API selbst ist sehr einfach. Es funktioniert genauso wie getUserMedia
, ermöglicht es Ihnen jedoch, einen Videostream von einer der definierten Anzeigeoberflächen aufzunehmen :
- vom Monitor (gesamter Bildschirm),
- aus einem Fenster oder allen Fenstern einer bestimmten Anwendung,
- von einem Browser oder vielmehr von einem bestimmten Dokument. In Chrome ist dieses Dokument eine separate Registerkarte, in FF gibt es jedoch keine solche Option.
Browser-API, mit der Sie über den Browser hinausblicken können ... Es kommt Ihnen bekannt vor und ist normalerweise auf einige Probleme zurückzuführen, aber in diesem Fall kann es sehr praktisch sein. Sie können ein Bild aus anderen Fenstern aufnehmen und beispielsweise Text in Echtzeit erkennen und übersetzen, z. B. mit Google Translate Camera. Nun, und es gibt wahrscheinlich noch viele weitere interessante Anwendungen.
Wir sammeln
Also haben wir herausgefunden, welche Funktionen uns die API bietet. Was weiter?
Und dann müssen wir diesen Videostream in Bilder überholen, an denen wir arbeiten können. Dazu verwenden wir die Elemente <video>
, <canvas>
und einige weitere JS.
Eine Nahaufnahme des Prozesses sieht ungefähr so aus:
- Direkter Stream zu
<video>
; - Zeichnen Sie mit einer bestimmten Häufigkeit den Inhalt des
<video>
in <canvas>
. - Sammeln Sie ein ImageData-Objekt aus
<canvas>
mithilfe der 2D-Kontextmethode getImageData
.
Diese ganze Prozedur mag aufgrund einer so langen Pipeline etwas seltsam klingen, aber diese Methode ist sehr beliebt und wurde verwendet, um Daten von Webcams in getUserMedia
zu erfassen.
Wenn wir alles Irrelevante weglassen, um den Stream zu starten und den Frame daraus herauszuziehen, benötigen wir ungefähr den folgenden Code:
async function run() { const video = document.createElement('video'); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); const displayMediaOptions = { video: { cursor: "never" }, audio: false } video.srcObject = await navigator.mediaDevices.getDisplayMedia(displayMediaOptions); const videoTrack = video.srcObject.getVideoTracks()[0]; const { height, width } = videoTrack.getSettings(); context.drawImage(video, 0, 0, width, height); return context.getImageData(0, 0, width, height); } await run();
Wie oben erwähnt: Zuerst erstellen wir die Elemente <video>
und <canvas>
und fragen die CanvasRenderingContext2D
nach einem 2D-Kontext ( CanvasRenderingContext2D
).
Dann definieren wir Durchflussbeschränkungen / -bedingungen . Im Gegensatz zu Streams von der Kamera gibt es nur wenige davon. Wir sagen, dass wir den Cursor nicht sehen wollen und dass wir kein Audio benötigen. Obwohl zum Zeitpunkt dieses Schreibens die Audioaufnahme von niemandem unterstützt wird.
Danach MediaStream
wir den empfangenen Stream vom Typ MediaStream
mit dem <video>
-Element. Beachten Sie, dass getDisplayMedia
ein Versprechen zurückgibt.
Aus den empfangenen Daten im Stream erinnern wir uns schließlich an die Auflösung des Videos, um es korrekt auf die Leinwand zu zeichnen, den Rahmen zu zeichnen und das ImageData-Objekt aus der ImageData
herauszuziehen.
Für die vollständige Verwendung möchten Sie höchstwahrscheinlich Frames in einer Schleife anstatt einmal verarbeiten. Zum Beispiel, während Sie warten, bis das gewünschte Bild im Rahmen erscheint. Und hier müssen ein paar Worte gesagt werden.
Wenn es darum geht, „etwas im DOM in einer konstanten Schleife zu behandeln“, fällt requestAnimationFrame
als erstes höchstwahrscheinlich requestAnimationFrame
. In unserem Fall funktioniert die Verwendung jedoch nicht. Die Sache ist, dass, wenn die Registerkarte nicht mehr aktiv ist, Browser die Verarbeitung der rAF-Schleife unterbrechen. In unserem Fall möchten wir zu diesem Zeitpunkt die Bilder verarbeiten.
In dieser Hinsicht werden wir anstelle von rAF das gute alte setInterval
. Aber bei ihm läuft es nicht so glatt. In einer inaktiven Registerkarte beträgt das Intervall zwischen Rückrufvorgängen mindestens 1 Sekunde . Trotzdem reicht uns das.
Wenn wir zu den Frames kommen, können wir sie nach Belieben verarbeiten. Für die Zwecke dieser Demo verwenden wir die jsQR- Bibliothek. Es ist sehr einfach: Die Eingabe akzeptiert ImageData
, die Breite und Höhe des Bildes. Wenn das empfangene Bild einen QR-Code hat, erhalten Sie ein JS-Objekt mit erkannten Daten zurück.
Ergänzen wir unser vorheriges Beispiel mit ein paar weiteren Codezeilen:
const imageData = await run(); const code = jsQR(imageData.data, streamWidth, streamHeight);
Fertig!
NPM
Ich dachte, dass der Hauptcode hinter diesem Beispiel in eine npm-Bibliothek gepackt werden könnte und bei der erstmaligen Verwendung Zeit für die spätere Verwendung sparen könnte. Die Bibliothek ist sehr einfach. In diesem Stadium akzeptiert sie nur den Rückruf, an den ImageData
gesendet wird, und ein zusätzlicher Parameter ist die Häufigkeit des Sendens von Daten. Alle Bearbeitungen müssen Sie selbst mitbringen. Ich werde darüber nachdenken, ob es sinnvoll ist, seine Funktionalität zu erweitern.
Die Bibliothek heißt stream-display
: NPM | Github .
Seine Verwendung reduziert sich auf buchstäblich drei Codezeilen und einen Rückruf:
const callback = imageData => {...}
Die Demo ist hier zu sehen. Es gibt auch eine CodePen- Version für schnelle Experimente. Beide Beispiele verwenden das obige NPM-Paket.
Ein bisschen über das Testen
Als ich diesen Code in die Bibliothek packte, musste ich mir überlegen, wie ich ihn testen sollte. Ich wollte absolut nicht 50 MB kopfloses Chrome ziehen, um ein paar kleine Tests darin durchzuführen. Und obwohl die Idee, Stubs für alle Komponenten zu schreiben, zu schmerzhaft schien, tat ich dies am Ende.
Als Testläufer wurde tape
ausgewählt. Folgendes musste ich endlich simulieren:
document
und DOM-Elemente. Dafür nahm ich jsdom ;- Einige jsdom-Methoden, die nicht implementiert sind:
HTMLMediaElement#play
, HTMLCanvasElement#getContext
und navigator.mediaDevices#getDisplayMedia
; - Zeit. Dazu habe ich die
useFakeTimers
Bibliothek verwendet, die unter der Haube lolex
nennt. Es setzt seine Ersetzungen auf setInterval
, requestAnimationFrame
und viele andere Funktionen, die mit der Zeit arbeiten, und ermöglicht es Ihnen, den Fluss dieser gefälschten Zeit zu steuern. Aber seien Sie vorsichtig: jsdom verwendet den Zeitablauf an einem Ort seines Initialisierungsprozesses, und wenn Sie zuerst sinon einschalten, friert alles ein.
Ich habe sinon auch für alle Funktionsstubs verwendet, die überwacht werden mussten. Der Rest wurde durch leere JS-Funktionen implementiert.
Natürlich können Sie die Tools auswählen, mit denen Sie bereits vertraut sind. Ich hoffe jedoch, dass Sie diese Liste im Voraus erstellen können, da Sie jetzt wissen, was Sie zu tun haben.
Das Endergebnis wird im Bibliotheks-Repository angezeigt. Es sieht nicht besonders hübsch aus, aber es funktioniert.
Fazit
Die Lösung erwies sich als nicht so elegant wie das am Anfang des Artikels erwähnte transparente Fenster, aber vielleicht wird das Web eines Tages dazu kommen. Man kann nur hoffen, dass diese Funktionen von Browsern streng kontrolliert werden, wenn sie lernen, durch ihre Fenster zu sehen. Denken Sie in der Zwischenzeit daran, dass der Bildschirm beim Fummeln in Chrome analysiert, aufgezeichnet usw. werden kann. Stöbern Sie also nicht mehr als nötig!
Ich hoffe, dass jemand nach diesem Artikel einen neuen Trick für sich selbst gelernt hat. Wenn Sie Ideen haben, was dies sonst noch verwendet werden kann, schreiben Sie in die Kommentare. Und bis bald.