Aufzeichnen eines Media-Elements

Während der Artikel zur Verwendung der MediaStream Recording API die Verwendung der MediaRecorder-Schnittstelle zur Erfassung eines durch ein Hardwaregerät erzeugten MediaStream demonstriert, wie er von navigator.mediaDevices.getUserMedia() zurückgegeben wird, können Sie auch ein HTML-Media-Element (nämlich <audio> oder <video>) als Quelle des aufzuzeichnenden MediaStream verwenden. In diesem Artikel betrachten wir ein Beispiel, das genau das tut.

Beispiel für die Aufzeichnung eines Media-Elements

HTML

Lassen Sie uns mit den wichtigsten Teilen des HTML beginnen. Es gibt noch ein wenig mehr als das, aber das ist nur informativ und nicht Teil des Hauptbetriebs der App.

html
<div class="left">
  <div id="startButton" class="button">Start Recording</div>
  <h2>Preview</h2>
  <video id="preview" width="160" height="120" autoplay muted></video>
</div>

Wir präsentieren unsere Hauptschnittstelle in zwei Spalten. Links befindet sich ein Startknopf und ein <video>-Element, das die Videovorschau anzeigt; dies ist das Video, das die Kamera des Benutzers sieht. Beachten Sie, dass das autoplay-Attribut verwendet wird, sodass das Video sofort angezeigt wird, sobald der Stream von der Kamera eintrifft, und das muted-Attribut angegeben ist, um sicherzustellen, dass der Ton des Mikrofons des Benutzers nicht über die Lautsprecher ausgegeben wird, was zu einer unschönen Rückkopplungsschleife führen würde.

html
<div class="right">
  <div id="stopButton" class="button">Stop Recording</div>
  <h2>Recording</h2>
  <video id="recording" width="160" height="120" controls></video>
  <a id="downloadButton" class="button">Download</a>
</div>

Rechts sehen wir einen Stop-Knopf und das <video>-Element, das zur Wiedergabe des aufgezeichneten Videos verwendet wird. Beachten Sie, dass das Wiedergabefeld kein Autoplay eingestellt hat (damit die Wiedergabe nicht sofort beginnt, sobald das Medium eintrifft) und es hat controls eingestellt, was bedeutet, dass dem Benutzer Steuerelemente zur Verfügung gestellt werden, um Play, Pause und so weiter zu bedienen.

Unter dem Wiedergabeelement befindet sich ein Knopf zum Herunterladen des aufgezeichneten Videos.

Nun schauen wir uns den JavaScript-Code an; hier passiert schließlich das meiste!

Festlegen globaler Variablen

Wir beginnen damit, einige globale Variablen zu erstellen, die wir benötigen.

js
let preview = document.getElementById("preview");
let recording = document.getElementById("recording");
let startButton = document.getElementById("startButton");
let stopButton = document.getElementById("stopButton");
let downloadButton = document.getElementById("downloadButton");
let logElement = document.getElementById("log");

let recordingTimeMS = 5000;

Die meisten davon sind Referenzen zu Elementen, mit denen wir arbeiten müssen. Die letzte, recordingTimeMS, ist auf 5000 Millisekunden (5 Sekunden) gesetzt; dies gibt die Länge der Videos an, die wir aufzeichnen werden.

Hilfsfunktionen

Als Nächstes erstellen wir einige Hilfsfunktionen, die später verwendet werden.

js
function log(msg) {
  logElement.innerText += `${msg}\n`;
}

Die log()-Funktion wird verwendet, um Textzeichenfolgen an ein <div> auszugeben, damit wir Informationen mit dem Benutzer teilen können. Nicht sehr hübsch, aber es erledigt den Job für unsere Zwecke.

js
function wait(delayInMS) {
  return new Promise((resolve) => setTimeout(resolve, delayInMS));
}

Die wait()-Funktion gibt ein neues Promise zurück, das aufgelöst wird, sobald die angegebene Anzahl von Millisekunden verstrichen ist. Es funktioniert, indem eine Pfeilfunktion verwendet wird, die setTimeout() aufruft und den Auflösungs-Handler des Promise als Timeout-Handler-Funktion angibt. Dadurch können wir die Promise-Syntax beim Verwenden von Zeitüberschreitungen nutzen, was sehr nützlich sein kann, wenn wir Promises verketten, wie wir später sehen werden.

Start der Medienaufnahme

Die startRecording()-Funktion behandelt den Start des Aufnahmevorgangs:

js
function startRecording(stream, lengthInMS) {
  let recorder = new MediaRecorder(stream);
  let data = [];

  recorder.ondataavailable = (event) => data.push(event.data);
  recorder.start();
  log(`${recorder.state} for ${lengthInMS / 1000} seconds…`);

  let stopped = new Promise((resolve, reject) => {
    recorder.onstop = resolve;
    recorder.onerror = (event) => reject(event.name);
  });

  let recorded = wait(lengthInMS).then(() => {
    if (recorder.state === "recording") {
      recorder.stop();
    }
  });

  return Promise.all([stopped, recorded]).then(() => data);
}

startRecording() nimmt zwei Eingabeparameter an: ein MediaStream, von dem aufgenommen werden soll, und die Länge in Millisekunden der zu erstellenden Aufnahme. Wir nehmen nie mehr als die angegebene Anzahl von Millisekunden auf, obwohl, wenn die Medien vorher stoppen, auch der MediaRecorder die Aufnahme automatisch stoppt.

  • Zuerst erstellen wir den MediaRecorder, der die Aufnahme des Eingabe-stream behandelt.
  • data ist ein Array, das zunächst leer ist und die Blobs der Mediendaten enthält, die unserem ondataavailable-Ereignishandler zur Verfügung gestellt werden.
  • Die ondataavailable-Zuweisung richtet den Handler für das dataavailable-Ereignis ein. Die data-Eigenschaft des empfangenen Ereignisses ist ein Blob, das die Mediendaten enthält. Der Ereignishandler schiebt den Blob auf das data-Array.
  • Wir starten den Aufnahmevorgang durch Aufruf von recorder.start() und geben eine Nachricht im Log aus, die den aktualisierten Zustand des Recorders und die Anzahl der Sekunden, die er aufnehmen wird, enthält.
  • Wir erstellen ein neues Promise, genannt stopped, das aufgelöst wird, wenn der onstop-Ereignishandler des MediaRecorder aufgerufen wird und das abgelehnt wird, wenn sein onerror-Ereignishandler aufgerufen wird. Der Ablehnungshandler erhält als Eingabe den Namen des aufgetretenen Fehlers.
  • Wir erstellen ein weiteres neues Promise, genannt recorded, das aufgelöst wird, wenn die angegebene Anzahl von Millisekunden verstrichen ist. Nach der Auflösung stoppt es den MediaRecorder, wenn er aufnimmt.
  • Schließlich verwenden wir Promise.all, um ein neues Promise zu erstellen, das erfüllt wird, wenn beide Promises (stopped und recorded) aufgelöst wurden. Sobald dies aufgelöst ist, wird das Array data von startRecording() an seinen Aufrufer zurückgegeben.

Stoppen des Eingabestreams

Die stop()-Funktion stoppt die Eingabemedien:

js
function stop(stream) {
  stream.getTracks().forEach((track) => track.stop());
}

Dies funktioniert, indem MediaStream.getTracks() aufgerufen wird, um mit forEach() auf jede Spur im Stream MediaStreamTrack.stop() aufzurufen.

Abrufen eines Eingabestreams und Einrichten des Recorders

Nun sehen wir uns den kompliziertesten Codeabschnitt in diesem Beispiel an: unseren Ereignishandler für Klicks auf den Startknopf:

js
startButton.addEventListener(
  "click",
  () => {
    navigator.mediaDevices
      .getUserMedia({
        video: true,
        audio: true,
      })
      .then((stream) => {
        preview.srcObject = stream;
        downloadButton.href = stream;
        preview.captureStream =
          preview.captureStream || preview.mozCaptureStream;
        return new Promise((resolve) => {
          preview.onplaying = resolve;
        });
      })
      .then(() => startRecording(preview.captureStream(), recordingTimeMS))
      .then((recordedChunks) => {
        let recordedBlob = new Blob(recordedChunks, { type: "video/webm" });
        recording.src = URL.createObjectURL(recordedBlob);
        downloadButton.href = recording.src;
        downloadButton.download = "RecordedVideo.webm";

        log(
          `Successfully recorded ${recordedBlob.size} bytes of ${recordedBlob.type} media.`,
        );
      })
      .catch((error) => {
        if (error.name === "NotFoundError") {
          log("Camera or microphone not found. Can't record.");
        } else {
          log(error);
        }
      });
  },
  false,
);

Wenn ein click-Ereignis eintritt, passiert Folgendes:

  • MediaDevices.getUserMedia wird aufgerufen, um einen neuen MediaStream anzufordern, der sowohl Video- als auch Audiotracks enthält. Dies ist der Stream, den wir aufzeichnen werden.

  • Wenn das von getUserMedia() zurückgegebene Promise aufgelöst wird, wird die srcObject-Eigenschaft des Vorschau-<video>-Elements auf den Eingabestream gesetzt, wodurch das von der Kamera des Benutzers aufgenommene Video im Vorschaukasten angezeigt wird. Da das <video>-Element stummgeschaltet ist, wird der Ton nicht abgespielt. Der Link des "Download"-Knopfes wird dann ebenfalls auf den Stream gesetzt. Dann arrangieren wir, dass preview.captureStream() preview.mozCaptureStream() aufruft, damit unser Code in Firefox funktioniert, bei dem die Methode HTMLMediaElement.captureStream() mit einem Präfix versehen ist. Dann wird ein neues Promise erstellt, das aufgelöst wird, wenn das Vorschauvideo zu spielen beginnt, und dieses wird zurückgegeben.

  • Wenn das Vorschauvideo zu spielen beginnt, wissen wir, dass es Medien zum Aufzeichnen gibt, also reagieren wir, indem wir die zuvor erstellte startRecording()-Funktion aufrufen und den Vorschau-Video-Stream (als aufzuzeichnende Medienquelle) und recordingTimeMS als Anzahl der Millisekunden, die aufgezeichnet werden sollen, übergeben. Wie zuvor erwähnt, gibt startRecording() ein Promise zurück, dessen Auflösungs-Handler (der ein Array von Blob-Objekten erhält, das die Teile der aufgezeichneten Mediendaten enthält) aufgerufen wird, sobald die Aufnahme abgeschlossen ist.

  • Der Auflösungs-Handler des Aufnahmeprozesses erhält als Eingabe ein Array von Mediendaten-Blobs, lokal bekannt als recordedChunks. Das erste, was wir tun, ist, die Teile zu einem einzigen Blob zusammenzuführen, dessen MIME-Typ "video/webm" ist, indem wir den Vorteil der Tatsache nutzen, dass der Blob()-Konstruktor Arrays von Objekten zu einem Objekt zusammenfügt. Dann wird URL.createObjectURL() verwendet, um eine URL zu erstellen, die auf den Blob verweist; dies wird dann der Wert des src-Attributs des Wiedergabe-Elements des aufgezeichneten Videos, damit Sie das Video aus dem Blob abspielen können, sowie das Ziel des Download-Buttons.

    Dann wird das download-Attribut des Download-Knopfes gesetzt. Während das download-Attribut ein Boolean sein kann, können Sie es auch auf eine Zeichenkette setzen, um es als Namen für die heruntergeladene Datei zu verwenden. Indem wir das download-Attribut des Download-Links auf "RecordedVideo.webm" setzen, sagen wir dem Browser, dass ein Klick auf die Schaltfläche eine Datei namens "RecordedVideo.webm" herunterlädt, deren Inhalt das aufgezeichnete Video ist.

  • Die Größe und der Typ der aufgezeichneten Medien werden im Logbereich unterhalb der beiden Videos und des Download-Knopfes ausgegeben.

  • Das catch() für alle Promises gibt den Fehler im Protokollbereich aus, indem es unsere log()-Funktion aufruft.

Umgang mit dem Stop-Knopf

Der letzte Codeabschnitt fügt einen Handler für das click-Ereignis auf dem Stop-Knopf mit addEventListener() hinzu:

js
stopButton.addEventListener(
  "click",
  () => {
    stop(preview.srcObject);
  },
  false,
);

Dies ruft die zuvor behandelte stop()-Funktion auf.

Ergebnis

Wenn alles mit dem Rest des nicht gezeigten HTML und CSS zusammengefügt wird, sieht es so aus und funktioniert so:

Sie können das vollständige Demo hier ansehen und die Entwicklertools Ihres Browsers verwenden, um die Seite zu inspizieren und sich den gesamten Code anzusehen, einschließlich der Teile, die oben verborgen sind, da sie nicht entscheidend für die Erklärung sind, wie die APIs verwendet werden.

Siehe auch