Fotos mit getUserMedia() aufnehmen
Dieser Artikel zeigt, wie Sie navigator.mediaDevices.getUserMedia()
verwenden können, um auf die Kamera eines Computers oder Mobiltelefons mit getUserMedia()
-Unterstützung zuzugreifen und damit ein Foto aufzunehmen.
Sie können auch direkt zum Demo springen, wenn Sie möchten.
Das HTML-Markup
Unser HTML-Interface hat zwei Hauptbetriebsteile: das Stream- und Aufnahmepanel und das Präsentationspanel. Jeder dieser Teile wird nebeneinander in seinem eigenen <div>
präsentiert, um das Styling und die Steuerung zu erleichtern.
Das erste Panel auf der linken Seite enthält zwei Komponenten: ein <video>
-Element, das den Stream von navigator.mediaDevices.getUserMedia()
empfängt, und ein <button>
, das der Benutzer klickt, um einen Videorahmen aufzunehmen.
<div class="camera">
<video id="video">Video stream not available.</video>
<button id="start-button">Take photo</button>
</div>
Dies ist unkompliziert, und wir werden sehen, wie es zusammenpasst, wenn wir in den JavaScript-Code eintauchen.
Als Nächstes haben wir ein <canvas>
-Element, in dem die aufgenommenen Rahmen gespeichert, möglicherweise in irgendeiner Weise manipuliert und dann in eine Ausgabebilddatei umgewandelt werden. Dieses Canvas wird durch Styling mit display: none
verborgen gehalten, um den Bildschirm nicht zu überladen — der Benutzer muss dieses Zwischenstadium nicht sehen.
Wir haben auch ein <img>
-Element, in das wir das Bild zeichnen — dies ist die endgültige Anzeige, die dem Benutzer gezeigt wird.
<canvas id="canvas"> </canvas>
<div class="output">
<img id="photo" alt="The screen capture will appear in this box." />
</div>
Das ist der gesamte relevante HTML-Code. Der Rest ist nur etwas Seitenlayout-Fluff und ein bisschen Text, der einen Link zurück zu dieser Seite bietet.
Der JavaScript-Code
Nun lassen Sie uns einen Blick auf den JavaScript-Code werfen. Wir werden ihn in einige verdauliche Stücke aufteilen, um die Erklärung zu erleichtern.
Initialisierung
Wir beginnen damit, das gesamte Skript in eine anonyme Funktion zu wickeln, um globale Variablen zu vermeiden, und dann verschiedene Variablen einzurichten, die wir verwenden werden.
(() => {
const width = 320; // We will scale the photo width to this
const height = 0; // This will be computed based on the input stream
const streaming = false;
let video = null;
let canvas = null;
let photo = null;
let startButton = null;
Diese Variablen sind:
width
-
Unabhängig von der Größe des eingehenden Videos werden wir das resultierende Bild skalieren, damit es 320 Pixel breit ist.
height
-
Die Ausgabehöhe des Bildes wird basierend auf der
width
und dem Seitenverhältnis des Streams berechnet. streaming
-
Zeigt an, ob derzeit ein aktiver Video-Stream läuft.
video
-
Dies wird ein Verweis auf das
<video>
-Element sein, nachdem die Seite geladen ist. canvas
-
Dies wird ein Verweis auf das
<canvas>
-Element sein, nachdem die Seite geladen ist. photo
-
Dies wird ein Verweis auf das
<img>
-Element sein, nachdem die Seite geladen ist. -
Dies wird ein Verweis auf das
<button>
-Element sein, das zur Auslösung der Aufnahme verwendet wird. Wir erhalten es, nachdem die Seite geladen ist.
Die startup()
-Funktion
Die startup()
-Funktion wird ausgeführt, wenn die Seite vollständig geladen ist, dank EventTarget.addEventListener
. Die Aufgabe dieser Funktion ist es, Zugriff auf die Webcam des Benutzers anzufordern, das Ausgangs-<img>
auf einen Standardzustand zu initialisieren und die erforderlichen Ereignis-Listener einzurichten, um jeden Videorahmen von der Kamera zu empfangen und zu reagieren, wenn der Button geklickt wird, um ein Bild aufzunehmen.
Referenzen der Elemente erhalten
Zuerst holen wir Referenzen zu den wichtigsten Elementen, auf die wir zugreifen müssen.
function startup() {
video = document.getElementById('video');
canvas = document.getElementById('canvas');
photo = document.getElementById('photo');
startButton = document.getElementById('start-button');
Den Medien-Stream erhalten
Die nächste Aufgabe ist es, den Medien-Stream zu erhalten:
navigator.mediaDevices
.getUserMedia({ video: true, audio: false })
.then((stream) => {
video.srcObject = stream;
video.play();
})
.catch((err) => {
console.error(`An error occurred: ${err}`);
});
Hier rufen wir MediaDevices.getUserMedia()
auf und fordern einen Video-Stream an (ohne Audio). Es gibt ein Promise zurück, an das wir Erfolgs- und Fehler-Callbacks anhängen.
Der Erfolgs-Callback empfängt ein stream
-Objekt als Eingabe. Es ist die Quelle unseres neuen Streams für das <video>
-Element.
Sobald der Stream an das <video>
-Element gebunden ist, starten wir ihn, indem wir HTMLMediaElement.play()
aufrufen.
Der Fehler-Callback wird aufgerufen, wenn das Öffnen des Streams nicht funktioniert. Dies passiert zum Beispiel, wenn keine kompatible Kamera angeschlossen ist oder der Benutzer den Zugriff verweigert hat.
Warten auf den Start der Video-Wiedergabe
Nach dem Aufruf von HTMLMediaElement.play()
auf dem <video>
folgt eine (hoffentlich kurze) Zeitspanne, bevor der Video-Stream zu fließen beginnt. Um ein Blockieren bis dahin zu vermeiden, fügen wir dem video
einen Ereignis-Listener für das canplay
-Ereignis hinzu, das ausgeliefert wird, wenn die Videowiedergabe tatsächlich beginnt. An diesem Punkt sind alle Eigenschaften im video
-Objekt basierend auf dem Format des Streams konfiguriert.
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 macht nichts, es sei denn, es ist das erste Mal, dass er aufgerufen wurde; dies wird getestet, indem auf den Wert unserer streaming
-Variablen geschaut wird, die beim ersten Aufruf dieser Methode false
ist.
Wenn dies tatsächlich das erste Mal ist, richten wir die Höhe des Videos basierend auf der Größenunterschied zwischen der tatsächlichen Größe des Videos, video.videoWidth
, und der Breite, in der wir es rendern werden, width
, ein.
Schließlich werden die width
und height
sowohl des Videos als auch des Canvas aufeinander abgestimmt, indem Element.setAttribute()
auf jede der beiden Eigenschaften auf jedem Element aufgerufen wird, und die Breiten und Höhen wie angemessen gesetzt werden. Schließlich setzen wir die streaming
-Variable auf true
, um zu verhindern, dass wir versehentlich diesen Einrichtungscode erneut ausführen.
Klicks auf den Button behandeln
Um jedes Mal ein Standbild aufzunehmen, wenn der Benutzer den startButton
klickt, müssen wir dem Button einen Ereignis-Listener hinzufügen, der aufgerufen wird, wenn das click
-Ereignis ausgegeben wird:
startButton.addEventListener(
"click",
(ev) => {
takePicture();
ev.preventDefault();
},
false,
);
Diese Methode ist einfach genug: sie ruft einfach unsere takePicture()
-Funktion auf, die weiter unten im Abschnitt Ein Frame aus dem Stream aufnehmen definiert ist, und ruft dann Event.preventDefault()
auf dem empfangenen Ereignis auf, um zu verhindern, dass der Klick mehr als einmal behandelt wird.
Den startup()
-Methode abschließen
Es gibt nur noch zwei Zeilen Code in der startup()
-Methode:
clearPhoto();
}
Hier rufen wir die clearPhoto()
-Methode auf, die wir weiter unten im Abschnitt Den Foto-Frame leeren beschreiben.
Den Foto-Frame leeren
Das Leeren des Foto-Frames beinhaltet das Erstellen eines Bildes und dann das Konvertieren in ein Format, das vom <img>
-Element, das den zuletzt aufgenommenen Frame anzeigt, verwendet werden kann. Dieser Code sieht so aus:
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);
}
Wir beginnen damit, eine Referenz auf das versteckte <canvas>
-Element zu erhalten, das wir für das Offscreen-Rendering verwenden. Anschließend setzen wir die fillStyle
zu #AAA
(ein ziemlich helles Grau) und füllen das gesamte Canvas mit dieser Farbe, indem wir fillRect()
aufrufen.
Zuletzt in dieser Funktion konvertieren wir das Canvas in ein PNG-Bild und rufen photo.setAttribute()
auf, um unser aufgenommene Bildbox das Bild anzuzeigen zu lassen.
Ein Frame aus dem Stream aufnehmen
Es gibt eine letzte Funktion zu definieren, und sie ist der Zweck der gesamten Übung: die takePicture()
-Funktion, deren Aufgabe es ist, den aktuell angezeigten Videorahmen aufzunehmen, in eine PNG-Datei zu konvertieren und ihn in der aufgenommenen Frame-Box anzuzeigen. Der Code sieht so aus:
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 generell immer, wenn wir mit dem Inhalt eines Canvas arbeiten müssen, beginnen wir damit, den 2D-Zeichenkontext für das versteckte Canvas zu holen.
Dann, wenn die Breite und Höhe beide nicht null sind (was bedeutet, dass mindestens potenziell gültige Bilddaten vorhanden sind), setzen wir die Breite und Höhe des Canvas, um sie der des aufgenommenen Rahmens anzupassen, und rufen dann drawImage()
auf, um den aktuellen Frame des Videos in den Kontext zu zeichnen, wobei das gesamte Canvas mit dem Frame-Bild gefüllt wird.
Hinweis:
Dies nutzt die Tatsache aus, dass die HTMLVideoElement
-Schnittstelle wie ein HTMLImageElement
für jede API aussieht, die ein HTMLImageElement
als Parameter akzeptiert, wobei der aktuelle Frame des Videos als Bildinhalt präsentiert wird.
Sobald das Canvas 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, um unser aufgenommene Bildbox das Bild anzeigen zu lassen.
Wenn kein gültiges Bild verfügbar ist (d. h. die width
und height
sind beide 0), leeren wir den Inhaltsbereich des aufgenommenen Frames, indem wir clearPhoto()
aufrufen.
Demo
HTML
<div class="content-area">
<h1>MDN - navigator.mediaDevices.getUserMedia(): Still photo capture demo</h1>
<p>
This example demonstrates how to set up a media stream using your built-in
webcam, fetch an image from that stream, and create a PNG using that image.
</p>
<div class="camera">
<video id="video">Video stream not available.</video>
<button id="start-button">Take photo</button>
</div>
<canvas id="canvas"> </canvas>
<div class="output">
<img id="photo" alt="The screen capture will appear in this box." />
</div>
<p>
Visit our article
<a
href="https://developer.mozilla.org/en-US/docs/Web/API/Media_Capture_and_Streams_API/Taking_still_photos">
Taking still photos with WebRTC</a
>
to learn more about the technologies used here.
</p>
</div>
CSS
#video {
border: 1px solid black;
box-shadow: 2px 2px 3px black;
width: 320px;
height: 240px;
}
#photo {
border: 1px solid black;
box-shadow: 2px 2px 3px black;
width: 320px;
height: 240px;
}
#canvas {
display: none;
}
.camera {
width: 340px;
display: inline-block;
}
.output {
width: 340px;
display: inline-block;
vertical-align: top;
}
#start-button {
display: block;
position: relative;
margin-left: auto;
margin-right: auto;
bottom: 32px;
background-color: rgb(0 150 0 / 50%);
border: 1px solid rgb(255 255 255 / 70%);
box-shadow: 0px 0px 1px 2px rgb(0 0 0 / 20%);
font-size: 14px;
font-family: "Lucida Grande", "Arial", sans-serif;
color: rgb(255 255 255 / 100%);
}
.content-area {
font-size: 16px;
font-family: "Lucida Grande", "Arial", sans-serif;
width: 760px;
}
JavaScript
(() => {
// The width and height of the captured photo. We will set the
// width to the value defined here, but the height will be
// calculated based on the aspect ratio of the input stream.
const width = 320; // We will scale the photo width to this
let height = 0; // This will be computed based on the input stream
// |streaming| indicates whether or not we're currently streaming
// video from the camera. Obviously, we start at false.
let streaming = false;
// The various HTML elements we need to configure or control. These
// will be set by the startup() function.
let video = null;
let canvas = null;
let photo = null;
let startButton = null;
function showViewLiveResultButton() {
if (window.self !== window.top) {
// Ensure that if our document is in a frame, we get the user
// to first open it in its own tab or window. Otherwise, it
// won't be able to request permission for camera access.
document.querySelector(".content-area").remove();
const button = document.createElement("button");
button.textContent = "View live result of the example code above";
document.body.append(button);
button.addEventListener("click", () => window.open(location.href));
return true;
}
return false;
}
function startup() {
if (showViewLiveResultButton()) {
return;
}
video = document.getElementById("video");
canvas = document.getElementById("canvas");
photo = document.getElementById("photo");
startButton = document.getElementById("start-button");
navigator.mediaDevices
.getUserMedia({ video: true, audio: false })
.then((stream) => {
video.srcObject = stream;
video.play();
})
.catch((err) => {
console.error(`An error occurred: ${err}`);
});
video.addEventListener(
"canplay",
(ev) => {
if (!streaming) {
height = video.videoHeight / (video.videoWidth / width);
// Firefox currently has a bug where the height can't be read from
// the video, so we will make assumptions if this happens.
if (isNaN(height)) {
height = width / (4 / 3);
}
video.setAttribute("width", width);
video.setAttribute("height", height);
canvas.setAttribute("width", width);
canvas.setAttribute("height", height);
streaming = true;
}
},
false,
);
startButton.addEventListener(
"click",
(ev) => {
takePicture();
ev.preventDefault();
},
false,
);
clearPhoto();
}
// Fill the photo with an indication that none has been
// captured.
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);
}
// Capture a photo by fetching the current contents of the video
// and drawing it into a canvas, then converting that to a PNG
// format data URL. By drawing it on an offscreen canvas and then
// drawing that to the screen, we can change its size and/or apply
// other changes before drawing it.
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();
}
}
// Set up our event listener to run the startup process
// once loading is complete.
window.addEventListener("load", startup, false);
})();
Ergebnis
Spaß mit Filtern
Da wir Bilder von der Webcam des Benutzers durch das Aufnehmen von Frames aus einem <video>
-Element erfassen, können wir sehr einfach Filter und lustige Effekte auf das Video anwenden. Wie sich herausstellt, wirken sich alle CSS-Filter, die Sie auf das Element mit der filter
-Eigenschaft anwenden, auch auf das aufgenommene Foto aus. Diese Filter können von einfach (das Bild schwarz-weiß machen) bis extrem (Gaussian Blur und Farbtonrotation) reichen.
Sie können mit diesem Effekt experimentieren, indem Sie zum Beispiel die Firefox-Entwicklerwerkzeuge' Style-Editor verwenden; siehe CSS-Filter bearbeiten für Details, wie dies zu tun ist.
Verwendung spezifischer Geräte
Sie können, falls erforderlich, die erlaubte Menge an Videoquellen auf ein spezifisches Gerät oder eine Reihe von Geräten beschränken. Um dies zu tun, rufen Sie MediaDevices.enumerateDevices
auf. Wenn das Versprechen mit einer Liste von MediaDeviceInfo
-Objekten erfüllt wird, die die verfügbaren Geräte beschreiben, finden Sie die, die Sie zulassen möchten, und spezifizieren Sie die entsprechenden deviceId
oder deviceId
s im MediaTrackConstraints
-Objekt, das an getUserMedia()
übergeben wird.