Aufzeichnen eines Medienelements

Während der Artikel "Using the MediaStream Recording API" die Verwendung der MediaRecorder-Schnittstelle zum Erfassen eines von einem Hardwaregerät erzeugten MediaStream demonstriert, wie er von navigator.mediaDevices.getUserMedia() bereitgestellt wird, können Sie auch ein HTML-Medienelement (nämlich <audio> oder <video>) als Quelle des aufzuzeichnenden MediaStream verwenden. In diesem Artikel sehen wir uns ein Beispiel an, das genau das tut.

Beispiel zur Aufzeichnung eines Medienelements

HTML

Beginnen wir mit den wesentlichen Teilen des HTML. Es gibt noch etwas mehr, aber das dient nur informativen Zwecken und ist nicht Teil der Kernfunktionalität der Anwendung.

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>

Unsere Hauptschnittstelle präsentieren wir in zwei Spalten. Auf der linken Seite befindet sich eine Starttaste und ein <video>-Element, das die Video-Vorschau anzeigt; dies ist das Video, das die Kamera des Benutzers sieht. Beachten Sie, dass das Attribut autoplay verwendet wird, sodass das Video sofort angezeigt wird, sobald der Stream von der Kamera eintrifft, und dass das Attribut muted angegeben ist, um sicherzustellen, dass der Ton des Mikrofons des Benutzers nicht auf deren Lautsprecher ausgegeben wird, was zu einer unschönen Rückkopplungsschleife führen könnte.

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 eine Stopptaste 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 gestartet wird, sobald Medien eintreffen) und dass es controls gesetzt hat, was anzeigt, dass es die Benutzersteuerungen zum Abspielen, Anhalten und dergleichen anzeigt.

Unter dem Wiedergabeelement befindet sich eine Schaltfläche zum Herunterladen des aufgezeichneten Videos.

Nun sehen wir uns den JavaScript-Code an; hier geschieht schließlich der Großteil der Aktionen!

Einrichtung globaler Variablen

Wir beginnen mit der Festlegung einiger globaler Variablen, die wir benötigen werden.

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) eingestellt; das 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 in ein <div> auszugeben, damit wir Informationen mit dem Benutzer teilen können. Nicht sehr hübsch, aber für unsere Zwecke ausreichend.

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

Die wait()-Funktion gibt ein neues Promise zurück, das aufgelöst wird, nachdem die angegebene Anzahl von Millisekunden vergangen ist. Sie funktioniert, indem sie eine Pfeilfunktion verwendet, die setTimeout() aufruft und den Auflösungs-Handler des Promise als die Timeout-Handler-Funktion angibt. Das lässt uns die Promise-Syntax bei der Verwendung von Timeouts nutzen, was sehr nützlich sein kann, wenn Promises verkettet werden sollen, wie wir später sehen werden.

Start der Medienaufzeichnung

Die Funktion startRecording() übernimmt 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: ein MediaStream, von dem aus aufgenommen werden soll, und die Länge der Aufnahme in Millisekunden. Wir zeichnen immer nur die angegebene Anzahl von Millisekunden auf, obwohl MediaRecorder das Aufnehmen automatisch stoppt, wenn die Medien vor Ablauf der Zeit stoppen.

  • Wir erstellen zuerst den MediaRecorder, der die Aufnahme des Eingabestreams verwaltet.
  • data ist ein Array, das zu Beginn leer ist und die Blobs der Mediendaten enthält, die unserem ondataavailable Event-Handler zur Verfügung gestellt werden.
  • Die ondataavailable-Zuweisung richtet den Handler für das dataavailable-Ereignis ein. Die empfangenen data-Eigenschaft des Ereignisses ist ein Blob, das die Mediendaten enthält. Der Ereignishandler fügt das Blob dem data-Array hinzu.
  • Wir starten den Aufnahmeprozess, indem wir recorder.start() aufrufen, und geben eine Nachricht im Protokoll aus, die den aktualisierten Zustand des Recorders und die Anzahl der Sekunden anzeigt, die er aufzeichnen wird.
  • Wir erstellen ein neues Promise mit dem Namen stopped, das aufgelöst wird, wenn der onstop-Event-Handler des MediaRecorder aufgerufen wird, und abgelehnt wird, wenn sein onerror-Event-Handler aufgerufen wird. Der Ablehnungshandler erhält als Eingabe den Namen des aufgetretenen Fehlers.
  • Wir erstellen ein weiteres neues Promise mit dem Namen recorded, das aufgelöst wird, wenn die angegebene Anzahl von Millisekunden vergangen ist. Bei der Auflösung stoppt es den MediaRecorder, falls dieser noch 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 Funktion stop() stoppt die Eingabemedien:

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

Dies geschieht, indem MediaStream.getTracks() aufgerufen wird, und forEach() verwendet wird, um MediaStreamTrack.stop() auf jedem Track im Stream aufzurufen.

Abrufen eines Eingabestreams und Einrichten des Recorders

Sehen wir uns nun das komplizierteste Stück Code in diesem Beispiel an: unseren Ereignishandler für Klicks auf die Starttaste:

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 auftritt, passiert Folgendes:

  • MediaDevices.getUserMedia wird aufgerufen, um einen neuen MediaStream anzufordern, der sowohl Video- als auch Audiotracks enthält. Das 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 erfasste Video im Vorschaufeld angezeigt wird. Da das <video>-Element stummgeschaltet ist, wird der Ton nicht abgespielt. Der Link der "Herunterladen"-Schaltfläche wird dann ebenfalls auf den Stream gesetzt. Dann arrangieren wir, dass preview.captureStream() preview.mozCaptureStream() aufruft, damit unser Code in Firefox funktioniert, auf dem die Methode HTMLMediaElement.captureStream() ein Präfix hat. Anschließend wird ein neues Promise erstellt und zurückgegeben, das aufgelöst wird, wenn das Vorschauvideo zu spielen beginnt.

  • Wenn das Vorschauvideo zu spielen beginnt, wissen wir, dass es Medien gibt, die aufgezeichnet werden können. Daher antworten wir, indem wir die zuvor erstellte Funktion startRecording() aufrufen und den Vorschaustream (als Quellmedien, die aufgenommen werden sollen) und recordingTimeMS als die Anzahl der Millisekunden, die aufgenommen werden sollen, übergeben. Wie bereits erwähnt, gibt startRecording() ein Promise zurück, dessen Auflösungshandler aufgerufen wird (und als Eingabe ein Array von Blob-Objekten mit den aufgezeichneten Mediendatenstücken erhält), wenn die Aufnahme abgeschlossen ist.

  • Der Auflösungshandler des Aufnahmeprozesses erhält als Eingabe ein Array von Mediendaten-Blobs, lokal bekannt als recordedChunks. Das Erste, was wir tun, ist, die Stücke zu einem einzigen Blob zusammenzuführen, dessen MIME-Typ "video/webm" ist, indem wir die Tatsache nutzen, dass der Blob()-Konstruktor Arrays von Objekten zu einem Objekt verkettet. Dann wird URL.createObjectURL() verwendet, um eine URL zu erstellen, die auf das Blob verweist; diese wird dann zum Wert des src-Attributs (damit Sie das Video aus dem Blob abspielen können) des Wiedergabefeldes des aufgezeichneten Videos sowie zum Ziel des Links der Download-Schaltfläche gemacht.

    Dann wird das download-Attribut der Download-Schaltfläche gesetzt. Während das download-Attribut ein Boolean sein kann, können Sie es auch auf eine Zeichenfolge setzen, die als Name für die heruntergeladene Datei verwendet werden soll. Durch das Setzen des download-Attributs des Download-Links auf "RecordedVideo.webm" teilen wir dem Browser mit, dass beim Klicken der Schaltfläche eine Datei namens "RecordedVideo.webm" heruntergeladen werden soll, deren Inhalt das aufgezeichnete Video ist.

  • Die Größe und der Typ des aufgezeichneten Mediums werden an den Protokollbereich unterhalb der beiden Videos und der Download-Schaltfläche ausgegeben.

  • Das catch() für alle Promises gibt den Fehler durch einen Aufruf unserer log()-Funktion an den Protokollbereich aus.

Handhabung der Stopptaste

Der letzte Codeabschnitt fügt einen Handler für das click-Ereignis auf der Stopptaste hinzu, indem addEventListener() verwendet wird:

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

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

Ergebnis

Wenn alles zusammengefügt wird, einschließlich des restlichen HTML und des oben nicht gezeigten CSS, sieht es so aus und funktioniert wie folgt:

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

Siehe auch