Aufnahme von Standbildern mit getUserMedia()

Dieser Artikel zeigt, wie Sie navigator.mediaDevices.getUserMedia() verwenden, um auf die Kamera eines Computers oder Mobiltelefons mit getUserMedia()-Unterstützung zuzugreifen und ein Foto damit aufzunehmen.

getUserMedia-basierte Bildaufnahme-App — links sehen wir einen Videostrom von einer Webcam und einen Fotoaufnahmeknopf, rechts das Standbildausgabe nach der Aufnahme

Sie können auch direkt zum Demo springen, wenn Sie möchten.

Das HTML-Markup

Unsere HTML-Oberfläche hat zwei Hauptbetriebssektionen: das Strom- und Aufnahmepanel und das Präsentationspanel. Jede dieser Sektionen wird nebeneinander in ihrem eigenen <div> präsentiert, um das Styling und die Kontrolle zu erleichtern. Es gibt ein <button>-Element (permissions-button), das wir später in JavaScript verwenden können, um dem Benutzer zu erlauben, die Kameraerlaubnisse für jedes Gerät mit getUserMedia() zuzulassen oder zu blockieren.

Das Feld auf der linken Seite enthält zwei Komponenten: ein <video>-Element, das den Strom von navigator.mediaDevices.getUserMedia() empfangen wird, und ein <button>, um die Videoaufnahme zu starten. Dies ist einfach, und wir werden sehen, wie es zusammenpasst, wenn wir in den JavaScript-Code eintauchen.

html
<div class="camera">
  <video id="video">Video stream not available.</video>
  <button id="start-button">Capture photo</button>
</div>

Weiter haben wir ein <canvas>-Element, in das die aufgenommenen Frames gespeichert, möglicherweise auf irgendeine Weise manipuliert und dann in eine Ausgabebilddatei umgewandelt werden. Diese Leinwand wird durch Styling mit display: none ausgeblendet, um den Bildschirm nicht zu überladen — der Benutzer muss diese Zwischenstufe nicht sehen.

Wir haben auch ein <img>-Element, in das wir das Bild zeichnen werden — dies ist die endgültige Anzeige, die dem Benutzer gezeigt wird.

html
<canvas id="canvas"></canvas>
<div class="output">
  <img id="photo" alt="The screen capture will appear in this box." />
</div>

Der JavaScript-Code

Sehen wir uns nun den JavaScript-Code an. Wir werden ihn in ein paar leicht erklärbare Teile aufteilen.

Initialisierung

Wir beginnen mit der Einrichtung verschiedener Variablen, die wir verwenden werden.

js
const width = 320; // We will scale the photo width to this
let height = 0; // This will be computed based on the input stream

let streaming = false;

const video = document.getElementById("video");
const canvas = document.getElementById("canvas");
const photo = document.getElementById("photo");
const startButton = document.getElementById("start-button");
const allowButton = document.getElementById("permissions-button");

Diese Variablen sind:

width

Unabhängig von der Größe des eingehenden Videos werden wir das resultierende Bild auf 320 Pixel Breite skalieren.

height

Die Ausgabebreite des Bildes wird unter Berücksichtigung der width und des Seitenverhältnisses des Stroms berechnet.

streaming

Gibt an, ob gerade ein aktiver Videostrom läuft oder nicht.

video

Eine Referenz auf das <video>-Element.

canvas

Eine Referenz auf das <canvas>-Element.

photo

Eine Referenz auf das <img>-Element.

startButton

Eine Referenz auf das <button>-Element, das zum Auslösen der Aufnahme verwendet wird.

allowButton

Eine Referenz auf das <button>-Element, das steuert, ob die Seite auf Geräte zugreifen kann oder nicht.

Holen des Medienstroms

Die nächste Aufgabe besteht darin, den Medienstrom zu erhalten: Wir definieren einen Ereignis-Listener, der MediaDevices.getUserMedia() aufruft und einen Videostream (ohne Audio) anfordert, wenn der Benutzer auf die Schaltfläche "Kamera zulassen" klickt. Es gibt ein Versprechen zurück, an das wir Erfolgs- und Fehler-Callbacks anhängen:

js
allowButton.addEventListener("click", () => {
  navigator.mediaDevices
    .getUserMedia({ video: true, audio: false })
    .then((stream) => {
      video.srcObject = stream;
      video.play();
    })
    .catch((err) => {
      console.error(`An error occurred: ${err}`);
    });
});

Der Erfolgs-Callback erhält ein stream-Objekt als Eingabe, das als Quelle unseres <video>-Elements festgelegt wird. Sobald der Strom mit dem <video>-Element verknüpft ist, starten wir es, indem wir HTMLMediaElement.play() aufrufen.

Der Fehler-Callback wird aufgerufen, wenn das Öffnen des Stroms nicht funktioniert. Dies geschieht beispielsweise, wenn keine kompatible Kamera angeschlossen ist oder der Benutzer den Zugriff verweigert hat.

Auf das Starten des Videos hören

Nach dem Aufrufen von HTMLMediaElement.play() auf dem <video>, vergeht (hoffentlich nur eine kurze) Zeit, bevor der Videostream zu fließen beginnt. Damit wir nicht blockieren, bis das passiert, fügen wir einen Ereignis-Listener für das video-Element für das canplay-Ereignis hinzu, das ausgeliefert wird, wenn die Videowiedergabe tatsächlich beginnt. Zu diesem Zeitpunkt sind alle Eigenschaften im video-Objekt basierend auf dem Format des Stroms konfiguriert.

js
video.addEventListener(
  "canplay",
  (ev) => {
    if (!streaming) {
      height = video.videoHeight / (video.videoWidth / width);

      video.setAttribute("width", width);
      video.setAttribute("height", height);
      canvas.setAttribute("width", width);
      canvas.setAttribute("height", height);
      streaming = true;
    }
  },
  false,
);

Dieser Callback tut nichts, es sei denn, es ist das erste Mal, dass er aufgerufen wird; dies wird getestet, indem der Wert unserer streaming-Variable überprüft wird, der beim ersten Aufruf dieser Methode false ist.

Wenn dies tatsächlich der erste Lauf ist, setzen wir die Höhe des Videos basierend auf dem Größenunterschied zwischen der tatsächlichen Größe des Videos, video.videoWidth, und der Breite, mit der wir es rendern, width.

Schließlich werden die width und height sowohl des Videos als auch der Leinwand aufeinander abgestimmt, indem Element.setAttribute() für jede der beiden Eigenschaften auf jedem Element aufgerufen wird, und Breiten und Höhen entsprechend gesetzt. Schließlich setzen wir die streaming-Variable auf true, um zu verhindern, dass wir dieses Setup versehentlich erneut ausführen.

Klicken auf den Button handhaben

Um bei jedem Klick auf den startButton ein Standbild aufzunehmen, müssen wir der Schaltfläche einen Ereignis-Listener hinzufügen, der beim Klicken aufgerufen wird:

js
startButton.addEventListener(
  "click",
  (ev) => {
    takePicture();
    ev.preventDefault();
  },
  false,
);

Diese Methode ist einfach: Sie ruft die takePicture()-Funktion auf, die im Abschnitt Ein Frame aus dem Stream aufnehmen unten definiert ist, und ruft dann Event.preventDefault() auf dem empfangenen Ereignis auf, um zu verhindern, dass der Klick mehr als einmal behandelt wird.

Löschen des Fotokastens

Das Löschen des Fotokastens beinhaltet das Erstellen eines Bildes und dann das Konvertieren in ein Format, das vom <img>-Element verwendet werden kann, das den zuletzt aufgenommenen Frame anzeigt. Der Code sieht folgendermaßen aus:

js
function clearPhoto() {
  const context = canvas.getContext("2d");
  context.fillStyle = "#AAA";
  context.fillRect(0, 0, canvas.width, canvas.height);

  const data = canvas.toDataURL("image/png");
  photo.setAttribute("src", data);
}

clearPhoto();

Wir beginnen mit dem Abrufen einer Referenz auf das versteckte <canvas>-Element, das wir für das Rendern außerhalb des Bildschirms verwenden. Als nächstes setzen wir das fillStyle auf #AAA (ein ziemlich helles Grau) und füllen die gesamte Leinwand mit dieser Farbe, indem wir fillRect() aufrufen.

Zuletzt in dieser Funktion konvertieren wir die Leinwand in ein PNG-Bild und rufen photo.setAttribute() auf, um unsere aufgezeichnete Standbildbox das Bild anzeigen zu lassen.

Ein Frame aus dem Stream aufnehmen

Es gibt eine letzte Funktion zu definieren, und dies ist der Punkt der gesamten Übung: die takePicture()-Funktion, deren Aufgabe es ist, den derzeit angezeigten Videoframe aufzunehmen, in eine PNG-Datei zu konvertieren und im aufgenommenen Framekasten anzuzeigen. Der Code sieht folgendermaßen aus:

js
function takePicture() {
  const context = canvas.getContext("2d");
  if (width && height) {
    canvas.width = width;
    canvas.height = height;
    context.drawImage(video, 0, 0, width, height);

    const data = canvas.toDataURL("image/png");
    photo.setAttribute("src", data);
  } else {
    clearPhoto();
  }
}

Wie immer, wenn wir mit den Inhalten einer Leinwand arbeiten müssen, beginnen wir damit, den 2D-Zeichenkontext für die versteckte Leinwand zu erhalten.

Dann, wenn die Breite und Höhe beide ungleich Null sind (d.h. es gibt potenziell gültige Bilddaten), setzen wir die Breite und Höhe der Leinwand auf die des aufgenommenen Frames, und rufen dann drawImage() auf, um den aktuellen Frame des Videos in den Kontext zu zeichnen und die gesamte Leinwand mit dem Framebild zu füllen.

Hinweis: Dies nutzt die Tatsache, dass die HTMLVideoElement-Schnittstelle jedem API, das ein HTMLImageElement als Parameter akzeptiert, wie ein HTMLImageElement aussieht, wobei der aktuelle Frame des Videos als Inhalt des Bildes präsentiert wird.

Sobald die Leinwand das aufgenommene Bild enthält, konvertieren wir es in das PNG-Format, indem wir HTMLCanvasElement.toDataURL() darauf aufrufen; schließlich rufen wir photo.setAttribute() auf, damit unsere aufgezeichnete Standbildbox das Bild anzeigt.

Wenn kein gültiges Bild verfügbar ist (d.h. sowohl die width als auch die height sind 0), löschen wir die Inhalte des aufgenommenen Framekastens, indem wir clearPhoto() aufrufen.

Demo

Klicken Sie auf "Kamera erlauben", um ein Eingabegerät auszuwählen und der Seite den Zugriff auf die Kamera zu erlauben. Sobald das Video startet, können Sie auf "Foto aufnehmen" klicken, um ein Standbild aus dem Stream als auf die Leinwand auf der rechten Seite gezeichnetes Bild aufzunehmen:

Spaß mit Filtern

Da wir Bilder aus der Webcam des Benutzers aufnehmen, indem wir Frames von einem <video>-Element erfassen, können wir lustige CSS filter-Effekte auf das Video mit Filtern anwenden. Diese Filter reichen von einfach (das Bild schwarz-weiß machen) bis komplex (Gaussianische Weichzeichnungen und Farbtonrotation).

css
#video {
  filter: grayscale(100%);
}

Damit die Videofilter auf das Foto angewendet werden, benötigt die takePicture()-Funktion die folgenden Änderungen.

js
function takePicture() {
  const context = canvas.getContext("2d");
  if (width && height) {
    canvas.width = width;
    canvas.height = height;

    // Get the computed CSS filter from the video element.
    // For example, it might return "grayscale(100%)"
    const videoStyles = window.getComputedStyle(video);
    const filterValue = videoStyles.getPropertyValue("filter");

    // Apply the filter to the canvas drawing context.
    // If there's no filter (i.e., it returns "none"), default to "none".
    context.filter = filterValue !== "none" ? filterValue : "none";

    context.drawImage(video, 0, 0, width, height);

    const dataUrl = canvas.toDataURL("image/png");
    photo.setAttribute("src", dataUrl);
  } else {
    clearPhoto();
  }
}

Sie können mit diesem Effekt spielen, indem Sie beispielsweise die Stil-Editor der Firefox-Entwicklertools verwenden; siehe CSS-Filter bearbeiten für Details, wie man dies tut.

Verwendung spezieller Geräte

Sie können bei Bedarf die Menge der erlaubten Videoquellen auf ein bestimmtes Gerät oder eine bestimmte Menge von Geräten beschränken. Dies erreichen Sie, indem Sie MediaDevices.enumerateDevices aufrufen. Wenn das Versprechen mit einem Array von MediaDeviceInfo-Objekten erfüllt wird, die die verfügbaren Geräte beschreiben, finden Sie die, die Sie zulassen möchten, und spezifizieren die entsprechenden deviceId oder deviceIds im MediaTrackConstraints-Objekt, das in getUserMedia() übergeben wird.

Siehe auch