Erstellen wir einen benutzerdefinierten Audio Player

Avatar of Idorenyin Udoh
Idorenyin Udoh am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

HTML verfügt über eine integrierte native Audio-Player-Oberfläche, die wir einfach mit dem <audio>-Element erhalten. Zeigen Sie auf eine Sounddatei und das war's. Wir können sogar mehrere Dateien für eine bessere Browserunterstützung angeben, sowie eine kleine CSS-Flexibilität, um Dinge zu gestalten, wie dem Audio-Player einen Rahmen, abgerundete Ecken und vielleicht etwas Padding und Margin zu geben.

Aber selbst mit all dem ... kann der gerenderte Audio-Player selbst ein wenig, wissen Sie, schlicht aussehen.

Wussten Sie, dass es möglich ist, einen benutzerdefinierten Audio-Player zu erstellen? Natürlich können wir das! Während der Standard-<audio>-Player in vielen Fällen großartig ist, kann ein benutzerdefinierter Player besser zu Ihnen passen, zum Beispiel wenn Sie einen Podcast betreiben und ein Audio-Player das Schlüsselelement auf einer Website für den Podcast ist. Schauen Sie sich den großartigen benutzerdefinierten Player an, den Chris und Dave auf der ShopTalk Show-Website eingerichtet haben.

Showing a black audio player with muted orange controls, including options to jump or rewind 30 seconds on each side of a giant play button, which sits on top of a timeline showing the audio current time and duration at both ends of the timeline, and an option to set the audio speed in the middle.
Der Audio-Player passt nahtlos zu anderen Elementen auf der Seite und bietet Bedienelemente, die das Gesamtdesign ergänzen.

In diesem Beitrag werden wir versuchen, unseren eigenen Player zu erstellen. Also, setzen Sie Ihre Kopfhörer auf, drehen Sie die Musik auf und legen wir los!

Die Elemente eines Audio-Players

Betrachten wir zuerst die standardmäßigen HTML-Audio-Player, die einige der gängigen Browser anbieten.

Google Chrome, Opera, and Microsoft Edge
Blink
Mozilla Firefox
Firefox
Internet Explorer
Internet Explorer

Wenn unser Ziel ist, die Funktionalität dieser Beispiele nachzubilden, dann müssen wir sicherstellen, dass unser Player Folgendes hat:

  • eine Wiedergabe-/Pause-Schaltfläche,
  • einen Suchregler,
  • die aktuelle Zeitanzeige,
  • die Dauer der Sounddatei,
  • eine Möglichkeit, den Ton stummzuschalten, und
  • einen Lautstärkeregler.

Nehmen wir an, das ist das Design, das wir anstreben.

Wir wollen nichts allzu Ausgefallenes: nur ein Machbarkeitsnachweis, den wir verwenden können, um zu demonstrieren, wie man etwas anderes als das standardmäßige HTML macht. Wir wollen nichts übermäßig Kompliziertes: nur ein Proof-of-Concept, mit dem wir zeigen können, wie man etwas anderes als das Standard-HTML erstellt.

Grundlegende Markup, Styling und Skripte für jedes Element

Bevor wir Funktionen erstellen und Dinge stylen, sollten wir zuerst die semantischen HTML-Elemente des Players durchgehen. Wir haben hier viele Elemente zur Verfügung, basierend auf den oben genannten Elementen.

Wiedergabe-/Pause-Schaltfläche

Ich denke, das HTML-Element, das für diese Schaltfläche geeignet ist, ist das <button>-Element. Es wird das Wiedergabe-Symbol enthalten, aber das Pausieren-Symbol sollte sich ebenfalls in dieser Schaltfläche befinden. Auf diese Weise wechseln wir zwischen beiden, anstatt Platz durch die Anzeige beider gleichzeitig zu verbrauchen.

So etwas im Markup

<div id="audio-player-container">
  <p>Audio Player</p>
  <!-- swaps with pause icon -->
  <button id="play-icon"></button>
</div>

Daher stellt sich die Frage: Wie wechseln wir zwischen den beiden Schaltflächen, sowohl visuell als auch funktional? Das Pausieren-Symbol ersetzt das Wiedergabe-Symbol, wenn die Wiedergabeaktion ausgelöst wird. Die Wiedergabe-Schaltfläche sollte angezeigt werden, wenn der Ton pausiert ist, und die Pausieren-Schaltfläche sollte angezeigt werden, wenn der Ton abgespielt wird.

Natürlich könnte eine kleine Animation stattfinden, während das Symbol vom Wiedergabe- zum Pausieren-Symbol wechselt. Was uns dabei helfen würde, ist Lottie, eine Bibliothek, die Adobe After Effects-Animationen nativ rendert. Wir müssen die Animation aber nicht in After Effects erstellen. Das animierte Symbol, das wir verwenden werden, wird von Icons8 kostenlos zur Verfügung gestellt.

Neu bei Lottie? Ich habe eine ausführliche Übersicht geschrieben, die erklärt, wie es funktioniert.

In der Zwischenzeit erlauben Sie mir, den folgenden Pen zu beschreiben.

Der HTML-Abschnitt enthält Folgendes:

  • ein Container für den Player,
  • Text, der den Container kurz beschreibt, und
  • ein <button>-Element für die Wiedergabe- und Pausieraktionen.

Der CSS-Abschnitt enthält einige leichte Stile. Das JavaScript ist das, was wir etwas aufschlüsseln müssen, weil es mehrere Dinge tut.

// imports the Lottie library via Skypack
import lottieWeb from 'https://cdn.skypack.dev/lottie-web';

// variable for the button that will contain both icons
const playIconContainer = document.getElementById('play-icon');
// variable that will store the button’s current state (play or pause)
let state = 'play';

// loads the animation that transitions the play icon into the pause icon into the referenced button, using Lottie’s loadAnimation() method
const animation = lottieWeb.loadAnimation({
  container: playIconContainer,
  path: 'https://maxst.icons8.com/vue-static/landings/animated-icons/icons/pause/pause.json',
  renderer: 'svg',
  loop: false,
  autoplay: false,
  name: "Demo Animation",
});

animation.goToAndStop(14, true);

// adds an event listener to the button so that when it is clicked, the the player toggles between play and pause
playIconContainer.addEventListener('click', () => {
  if(state === 'play') {
    animation.playSegments([14, 27], true);
    state = 'pause';
  } else {
    animation.playSegments([0, 14], true);
    state = 'play';
  }
});

Hier ist, was das Skript tut, abzüglich des Codes:

  • Es importiert die Lottie-Bibliothek über Skypack.
  • Es referenziert die Schaltfläche, die beide Symbole enthalten wird, in einer Variablen.
  • Es definiert eine Variable, die den aktuellen Zustand der Schaltfläche (Wiedergabe oder Pause) speichern wird.
  • Es lädt die Animation, die das Wiedergabe-Symbol in das Pausieren-Symbol umwandelt, in die referenzierte Schaltfläche, unter Verwendung der loadAnimation()-Methode von Lottie.
  • Es zeigt das Wiedergabe-Symbol beim Laden an, da der Ton zunächst pausiert ist.
  • Es fügt einen Event-Listener zur Schaltfläche hinzu, sodass beim Klicken der Player zwischen Wiedergabe und Pause wechselt.

Aktuelle Zeit und Dauer

Die aktuelle Zeit ist wie eine Fortschrittsanzeige, die Ihnen zeigt, wie viel Zeit seit dem Start der Sounddatei vergangen ist. Die Dauer? Das ist einfach, wie lang die Sounddatei ist.

Ein <span>-Element ist in Ordnung, um diese anzuzeigen. Das <span>-Element für die aktuelle Zeit, das jede Sekunde aktualisiert wird, hat einen Standardtextinhalt von 0:00. Auf der anderen Seite ist das für die Dauer die Dauer des Audios im mm:ss-Format.

<div id="audio-player-container">
  <p>Audio Player</p>
  <button id="play-icon"></button>
  <span id="current-time" class="time">0:00</span>
  <span id="duration" class="time">0:00</span>
</div>

Suchregler und Lautstärkeregler

Wir brauchen eine Möglichkeit, zu einem beliebigen Zeitpunkt in der Sounddatei zu springen. Wenn ich also zum Mittelpunkt der Datei springen möchte, kann ich einfach einen Regler anklicken und zu dieser Stelle in der Zeitachse ziehen.

Wir brauchen auch eine Möglichkeit, die Lautstärke zu steuern. Das kann auch eine Art Klick-und-Zieh-Regler sein.

Ich würde sagen, <input type="range"> ist das richtige HTML-Element für beide Funktionen.

<div id="audio-player-container">
  <p>Audio Player</p>
  <button id="play-icon"></button>
  <span id="current-time" class="time">0:00</span>
  <input type="range" id="seek-slider" max="100" value="0">
  <span id="duration" class="time">0:00</span>
  <input type="range" id="volume-slider" max="100" value="100">
</div>

Das Styling von Range-Inputs mit CSS ist absolut möglich, aber ich sage Ihnen was: Es fällt mir schwer, das zu begreifen. Dieser Artikel wird helfen. Großen Respekt an dich, Ana. Die Handhabung der Browserunterstützung mit all diesen Vendor-Präfixen ist an sich schon ein CSS-Trick. Schauen Sie sich den gesamten Code an, der für input[type="range"] benötigt wird, um eine konsistente Erfahrung zu erzielen.

input[type="range"] {
  position: relative;
  -webkit-appearance: none;
  width: 48%;
  margin: 0;
  padding: 0;
  height: 19px;
  margin: 30px 2.5% 20px 2.5%;
  float: left;
  outline: none;
}
input[type="range"]::-webkit-slider-runnable-track {
  width: 100%;
  height: 3px;
  cursor: pointer;
  background: linear-gradient(to right, rgba(0, 125, 181, 0.6) var(--buffered-width), rgba(0, 125, 181, 0.2) var(--buffered-width));
}
input[type="range"]::before {
  position: absolute;
  content: "";
  top: 8px;
  left: 0;
  width: var(--seek-before-width);
  height: 3px;
  background-color: #007db5;
  cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
  position: relative;
  -webkit-appearance: none;
  box-sizing: content-box;
  border: 1px solid #007db5;
  height: 15px;
  width: 15px;
  border-radius: 50%;
  background-color: #fff;
  cursor: pointer;
  margin: -7px 0 0 0;
}
input[type="range"]:active::-webkit-slider-thumb {
  transform: scale(1.2);
  background: #007db5;
}
input[type="range"]::-moz-range-track {
  width: 100%;
  height: 3px;
  cursor: pointer;
  background: linear-gradient(to right, rgba(0, 125, 181, 0.6) var(--buffered-width), rgba(0, 125, 181, 0.2) var(--buffered-width));
}
input[type="range"]::-moz-range-progress {
  background-color: #007db5;
}
input[type="range"]::-moz-focus-outer {
  border: 0;
}
input[type="range"]::-moz-range-thumb {
  box-sizing: content-box;
  border: 1px solid #007db5;
  height: 15px;
  width: 15px;
  border-radius: 50%;
  background-color: #fff;
  cursor: pointer;
}
input[type="range"]:active::-moz-range-thumb {
  transform: scale(1.2);
  background: #007db5;
}
input[type="range"]::-ms-track {
  width: 100%;
  height: 3px;
  cursor: pointer;
  background: transparent;
  border: solid transparent;
  color: transparent;
}
input[type="range"]::-ms-fill-lower {
  background-color: #007db5;
}
input[type="range"]::-ms-fill-upper {
  background: linear-gradient(to right, rgba(0, 125, 181, 0.6) var(--buffered-width), rgba(0, 125, 181, 0.2) var(--buffered-width));
}
input[type="range"]::-ms-thumb {
  box-sizing: content-box;
  border: 1px solid #007db5;
  height: 15px;
  width: 15px;
  border-radius: 50%;
  background-color: #fff;
  cursor: pointer;
}
input[type="range"]:active::-ms-thumb {
  transform: scale(1.2);
  background: #007db5;
}

Wow! Was bedeutet das überhaupt, richtig?

Das Styling des Fortschrittsbereichs von Range-Inputs ist ein kniffliges Unterfangen. Firefox bietet das Pseudo-Element ::-moz-range-progress, während Internet Explorer ::-ms-fill-lower bietet. Da WebKit-Browser kein ähnliches Pseudo-Element anbieten, müssen wir das Pseudo-Element ::before verwenden, um den Fortschritt zu improvisieren. Das erklärt, warum ich, wenn Sie es bemerkt haben, Event-Listener im JavaScript-Abschnitt hinzugefügt habe, um benutzerdefinierte CSS-Eigenschaften (z. B. --before-width) festzulegen, die aktualisiert werden, wenn das Input-Event für jeden der Regler ausgelöst wird.

Eines der nativen HTML <audio>-Beispiele, das wir uns zuvor angesehen haben, zeigt die gepufferte Menge des Audios. Die Eigenschaft --buffered-width gibt den Prozentsatz des Audios an, den der Benutzer abspielen kann, ohne auf den Download durch den Browser warten zu müssen. Ich habe diese Funktion mit der Funktion linear-gradient() auf der Spur des Suchreglers nachgeahmt. Ich habe die Funktion rgba() in der Funktion linear-gradient() für die Farbstopps verwendet, um Transparenz zu zeigen. Die gepufferte Breite hat eine viel tiefere Farbe im Vergleich zum Rest der Spur. Die tatsächliche Implementierung dieser Funktion würden wir jedoch viel später behandeln.

Lautstärke in Prozent

Dies dient zur Anzeige der prozentualen Lautstärke. Der Textinhalt dieses Elements wird aktualisiert, während der Benutzer die Lautstärke über den Regler ändert. Da es auf Benutzereingaben basiert, denke ich, dass dieses Element das <output>-Element sein sollte.

<div id="audio-player-container">
  <p>Audio Player</p>
  <button id="play-icon"></button>
  <span id="current-time" class="time">0:00</span>
  <input type="range" id="seek-slider" max="100" value="0">
  <span id="duration" class="time">0:00</span>
  <output id="volume-output">100</output>
  <input type="range" id="volume-slider" max="100" value="100">
</div>

Stummschaltungsschaltfläche

Wie bei den Wiedergabe- und Pausieraktionen sollte dies in einem <button>-Element erfolgen. Glücklicherweise hat Icons8 auch ein animiertes Stummschaltungssymbol. Wir würden also die Lottie-Bibliothek hier genauso verwenden wie für die Wiedergabe-/Pause-Schaltfläche.

<div id="audio-player-container">
  <p>Audio Player</p>
  <button id="play-icon"></button>
  <span id="current-time" class="time">0:00</span>
  <input type="range" id="seek-slider" max="100" value="0">
  <span id="duration" class="time">0:00</span>
  <output id="volume-output">100</output>
  <input type="range" id="volume-slider" max="100" value="100">
  <button id="mute-icon"></button>
</div>

Das ist alles, was wir im Moment an grundlegender Markup, Styling und Scripting benötigen!

An der Funktionalität arbeiten

Das HTML-<audio>-Element hat ein preload-Attribut. Dieses Attribut gibt dem Browser Anweisungen, wie die Audiodatei geladen werden soll. Es akzeptiert einen von drei Werten:

  • none – gibt an, dass der Browser das Audio nicht laden soll (es sei denn, der Benutzer initiiert die Wiedergabeaktion)
  • metadata – gibt an, dass nur die Metadaten (wie die Dauer) des Audios geladen werden sollen
  • auto – lädt die vollständige Audiodatei

Ein leerer String ist gleichbedeutend mit dem Wert auto. Beachten Sie jedoch, dass diese Werte lediglich Hinweise für den Browser sind. Der Browser muss diesen Werten nicht zustimmen. Wenn sich ein Benutzer beispielsweise in einem Mobilfunknetz auf iOS befindet, lädt Safari keinen Teil eines Audios, unabhängig vom preload-Attribut, es sei denn, der Benutzer löst die Wiedergabeaktion aus. Für diesen Player würden wir den Wert metadata verwenden, da er nicht viel Overhead erfordert und wir die Dauer des Audios anzeigen möchten.

Was uns helfen wird, die Funktionen zu realisieren, die unser Audio-Player haben sollte, ist die JavaScript HTMLMediaElement-Schnittstelle, von der die HTMLAudioElement-Schnittstelle erbt. Damit unser Audio-Player-Code so selbsterklärend wie möglich ist, würde ich das JavaScript in zwei Abschnitte unterteilen: Präsentation und Funktionalität.

Zuerst sollten wir ein <audio>-Element im Audio-Player erstellen, das die grundlegenden Funktionen hat, die wir wollen.

<div id=”audio-player-container”>
  <audio src=”my-favourite-song.mp3” preload=”metadata” loop>
  <button id="play-icon"></button>
  <!-- ... -->
</div>

Anzeige der Audiolänge

Als Erstes wollen wir im Browser die Dauer des Audios anzeigen, sobald sie verfügbar ist. Die HTMLAudioElement-Schnittstelle hat eine duration-Eigenschaft, die die Dauer des Audios in Sekunden zurückgibt. Wenn sie nicht verfügbar ist, gibt sie NaN zurück.

Wir haben preload in diesem Beispiel auf metadata gesetzt, daher sollte der Browser uns diese Information beim Laden direkt zur Verfügung stellen ... vorausgesetzt, er respektiert preload. Da wir sicher sein werden, dass die Dauer verfügbar ist, wenn der Browser die Metadaten des Audios heruntergeladen hat, zeigen wir sie in unserem Handler für das Ereignis loadedmetadata an, das die Schnittstelle ebenfalls bereitstellt.

const audio = document.querySelector('audio');

audio.addEventListener('loadedmetadata', () => {
  displayAudioDuration(audio.duration);
});

Das ist großartig, aber wieder, wir bekommen die Dauer in Sekunden. Wir sollten das wahrscheinlich in ein mm:ss-Format umwandeln.

const calculateTime = (secs) => {
  const minutes = Math.floor(secs / 60);
  const seconds = Math.floor(secs % 60);
  const returnedSeconds = seconds < 10 ? `0${seconds}` : `${seconds}`;
  return `${minutes}:${returnedSeconds}`;
}

Wir verwenden Math.floor(), weil die von der duration-Eigenschaft zurückgegebenen Sekunden normalerweise Dezimalzahlen sind.

Die dritte Variable, returnedSeconds, ist notwendig für Situationen, in denen die Dauer beispielsweise 4 Minuten und 8 Sekunden beträgt. Wir möchten 4:08 zurückgeben, nicht 4:8.

Häufig lädt der Browser das Audio schneller als üblich. Wenn dies geschieht, wird das loadedmetadata-Ereignis ausgelöst, bevor sein Listener dem <audio>-Element hinzugefügt werden kann. Daher wird die Audiolänge nicht im Browser angezeigt. Dennoch gibt es einen Hack. Das HTMLMediaElement hat eine Eigenschaft namens readyState. Sie gibt eine Zahl zurück, die laut MDN Web Docs den Bereitschaftszustand des Mediums anzeigt. Das Folgende beschreibt die Werte:

  • 0 – keine Daten über das Medium verfügbar.
  • 1 – die Metadatenattribute des Mediums sind verfügbar.
  • 2 – Daten sind verfügbar, aber nicht genug, um mehr als einen Frame abzuspielen.
  • 3 – Daten sind verfügbar, aber nur für eine geringe Anzahl von Frames ab der aktuellen Wiedergabeposition.
  • 4 – Daten sind verfügbar, so dass das Medium bis zum Ende ohne Unterbrechung abgespielt werden kann.

Wir wollen uns auf die Metadaten konzentrieren. Unser Ansatz ist also, die Dauer anzuzeigen, wenn die Metadaten des Audios verfügbar sind. Wenn sie nicht verfügbar sind, fügen wir den Event-Listener hinzu. Auf diese Weise wird die Dauer immer angezeigt.

const audio = document.querySelector('audio');
const durationContainer = document.getElementById('duration');

const calculateTime = (secs) => {
  const minutes = Math.floor(secs / 60);
  const seconds = Math.floor(secs % 60);
  const returnedSeconds = seconds < 10 ? `0${seconds}` : `${seconds}`;
  return `${minutes}:${returnedSeconds}`;
}

const displayDuration = () => {
  durationContainer.textContent = calculateTime(audio.duration);
}

if (audio.readyState > 0) {
  displayDuration();
} else {
  audio.addEventListener('loadedmetadata', () => {
    displayDuration();
  });
}

Suchregler

Der Standardwert der max-Eigenschaft des Range-Sliders ist 100. Die allgemeine Idee ist, dass der Thumb sich „gleitend“ bewegen soll, wenn der Ton abgespielt wird. Außerdem soll er sich jede Sekunde bewegen, so dass er am Ende des Reglers ankommt, wenn der Ton endet.

Ungeachtet dessen, wenn die Audiolänge 150 Sekunden beträgt und der Wert der max-Eigenschaft des Reglers 100 ist, erreicht der Thumb das Ende des Reglers, bevor der Ton endet. Deshalb ist es notwendig, den Wert der max-Eigenschaft des Reglers auf die Audiolänge in Sekunden einzustellen. Auf diese Weise erreicht der Thumb das Ende des Reglers, wenn der Ton endet. Erinnern Sie sich daran, dass dies geschehen sollte, wenn die Audiolänge verfügbar ist, wenn der Browser die Audio-Metadaten heruntergeladen hat, wie im Folgenden:

const seekSlider = document.getElementById('seek-slider');

const setSliderMax = () => {
  seekSlider.max = Math.floor(audio.duration);
}

if (audio.readyState > 0) {
  displayDuration();
  setSliderMax();
} else {
  audio.addEventListener('loadedmetadata', () => {
    displayDuration();
    setSliderMax();
  });
}

Pufferte Menge

Während der Browser das Audio herunterlädt, wäre es schön für den Benutzer zu wissen, wie viel davon er ohne Verzögerung ansteuern kann. Die HTMLMediaElement-Schnittstelle bietet die Eigenschaften buffered und seekable. Die buffered-Eigenschaft gibt ein TimeRanges-Objekt zurück, das die Teile des Mediums angibt, die der Browser heruntergeladen hat. Laut MDN Web Docs ist ein TimeRanges-Objekt eine Reihe von nicht überlappenden Zeitbereichen mit Start- und Endzeiten. Die Chunks sind normalerweise zusammenhängend, es sei denn, der Benutzer springt zu einem anderen Teil des Mediums. Die seekable-Eigenschaft gibt ein TimeRanges-Objekt zurück, das die „ansteuerbaren“ Teile des Mediums angibt, unabhängig davon, ob sie heruntergeladen wurden oder nicht.

Erinnern Sie sich, dass das Attribut preload="metadata" in unserem <audio>-Element vorhanden ist. Wenn beispielsweise die Audiolänge 100 Sekunden beträgt, gibt die buffered-Eigenschaft ein TimeRanges-Objekt ähnlich dem folgenden zurück:

Displays a line from o to 100, with a marker at 20. A range from 0 to 20 is highlighted on the line.

Wenn die Wiedergabe des Audios begonnen hat, gibt die seekable-Eigenschaft ein TimeRanges-Objekt ähnlich dem folgenden zurück:

Another timeline, but with four different ranges highlighted on the line, 0 to 20, 30 to 40, 60 to 80, and 90 to 100.

Es werden mehrere Chunks von Medien zurückgegeben, da häufig Byte-Range-Anfragen am Server aktiviert sind. Das bedeutet, dass mehrere Teile des Mediums gleichzeitig heruntergeladen werden können. Wir möchten jedoch die gepufferte Menge anzeigen, die der aktuellen Wiedergabeposition am nächsten liegt. Das wäre der erste Chunk (Zeitbereich 0 bis 20). Das wäre der erste und letzte Chunk aus dem ersten Bild. Während das Audio zu spielen beginnt, lädt der Browser weitere Chunks herunter. Wir möchten denjenigen anzeigen, der der aktuellen Wiedergabeposition am nächsten liegt, was der aktuelle letzte Chunk wäre, der von der gepufferten Eigenschaft zurückgegeben wird. Der folgende Ausschnitt speichert den Zeitwert für das Ende des letzten Bereichs im TimeRanges-Objekt, das von der buffered-Eigenschaft zurückgegeben wird, in der Variablen bufferedAmount.

const audio = document.querySelector('audio');
const bufferedAmount = audio.buffered.end(audio.buffered.length - 1);

Dies wäre 20 aus dem Bereich 0 bis 20 im ersten Bild. Der folgende Ausschnitt speichert den Zeitwert für das Ende des letzten Bereichs im TimeRanges-Objekt, das von der seekable-Eigenschaft zurückgegeben wird, in der Variablen seekableAmount.

const audio = document.querySelector('audio');
const seekableAmount = audio.seekable.end(audio.seekable.length - 1);

Dies wäre jedoch 100 aus dem Bereich 90 bis 100 im zweiten Bild, was der gesamten Audiolänge entspricht. Beachten Sie, dass es einige Lücken im TimeRanges-Objekt gibt, da der Browser nur einige Teile des Audios herunterlädt. Das bedeutet, dass die gesamte Dauer dem Benutzer als gepufferte Menge angezeigt wird. Inzwischen sind einige Teile des Audios noch nicht verfügbar. Da dies keine optimale Benutzererfahrung bietet, sollten wir den ersten Ausschnitt verwenden.

Während der Browser das Audio herunterlädt, sollte der Benutzer erwarten, dass die gepufferte Menge auf dem Regler breiter wird. Das HTMLMediaElement bietet ein Ereignis, das Progress-Ereignis, das ausgelöst wird, wenn der Browser das Medium lädt. Natürlich denke ich, was Sie denken! Die gepufferte Menge sollte im Handler für das Progress-Ereignis des Audios inkrementiert werden.

Schließlich sollten wir die gepufferte Menge tatsächlich auf dem Suchregler anzeigen. Dies tun wir, indem wir die Eigenschaft, über die wir zuvor gesprochen haben, --buffered-width, als Prozentsatz des Werts der max-Eigenschaft des Reglers festlegen. Ja, ebenfalls im Handler für das Progress-Ereignis. Und weil der Browser das Audio schneller als üblich lädt, sollten wir die Eigenschaft im loadedmetadata-Ereignis und seinem vorhergehenden bedingten Block, der den Bereitschaftszustand des Audios prüft, aktualisieren. Der folgende Pen kombiniert alles, was wir bisher behandelt haben.

Aktuelle Zeit

Während der Benutzer den Thumb entlang des Range-Inputs schiebt, sollte der Range-Wert im <span>-Element, das die aktuelle Zeit des Audios enthält, reflektiert werden. Dies teilt dem Benutzer die aktuelle Wiedergabeposition des Audios mit. Dies tun wir im Handler des Input-Event-Listeners des Reglers.

Wenn Sie denken, das richtige Ereignis, auf das man hören sollte, ist das change-Ereignis, ich widerspreche. Sagen wir, der Benutzer bewegt den Thumb vom Wert 0 zu 20. Das Input-Ereignis wird bei den Werten 1 bis 20 ausgelöst. Das Change-Ereignis wird jedoch nur bei Wert 20 ausgelöst. Wenn wir das Change-Ereignis verwenden, werden die Wiedergabepositionen von den Werten 1 bis 19 nicht reflektiert. Daher halte ich das Input-Ereignis für angemessen. Dann übergeben wir im Handler für das Ereignis den Wert des Reglers an die zuvor definierte Funktion calculateTime().

Wir haben die Funktion erstellt, um Zeit in Sekunden zu nehmen und sie in einem mm:ss-Format zurückzugeben. Wenn Sie denken: Oh, aber der Wert des Reglers ist keine Zeit in Sekunden, lassen Sie mich das erklären. Tatsächlich ist es das. Erinnern Sie sich, dass wir den Wert der max-Eigenschaft des Reglers auf die Audiolänge gesetzt haben, wenn sie verfügbar ist. Nehmen wir an, die Audiolänge beträgt 100 Sekunden. Wenn der Benutzer den Thumb zur Mitte des Reglers schiebt, beträgt der Wert des Reglers 50. Wir möchten nicht, dass 50 in der aktuellen Zeitanzeige erscheint, da dies nicht mit dem mm:ss-Format übereinstimmt. Wenn wir 50 an die Funktion übergeben, gibt die Funktion 0:50 zurück, und das wäre eine bessere Darstellung der Wiedergabeposition.

Ich habe den folgenden Ausschnitt zu unserem JavaScript hinzugefügt.

const currentTimeContainer = document.getElementById('current-time');

seekSlider.addEventListener('input', () => {
  currentTimeContainer.textContent = calculateTime(seekSlider.value);
});

Um es in Aktion zu sehen, können Sie den Thumb des Suchreglers im folgenden Pen hin und her bewegen.

Wiedergabe/Pause

Nun werden wir den Ton so einstellen, dass er entsprechend der vom Benutzer ausgelösten Aktion abgespielt oder pausiert wird. Wenn Sie sich erinnern, haben wir eine Variable, playState, erstellt, um den Zustand der Schaltfläche zu speichern. Diese Variable wird uns helfen zu wissen, wann der Ton wiedergegeben oder pausiert werden soll. Wenn ihr Wert play ist und die Schaltfläche geklickt wird, soll unser Skript die folgenden Aktionen ausführen:

  • den Ton wiedergeben
  • das Symbol von Wiedergabe auf Pause ändern
  • den Wert von playState auf pause ändern

Die zweite und dritte Aktion haben wir bereits im Handler für das Klickereignis der Schaltfläche implementiert. Was wir tun müssen, ist, die Anweisungen zum Abspielen und Pausieren des Audios im Event-Handler hinzuzufügen.

playIconContainer.addEventListener('click', () => {
  if(playState === 'play') {
    audio.play();
    playAnimation.playSegments([14, 27], true);
    playState = 'pause';
  } else {
    audio.pause();
    playAnimation.playSegments([0, 14], true);
    playState = 'play';
  }
});

Es ist möglich, dass der Benutzer zu einem bestimmten Teil des Audios springen möchte. In diesem Fall setzen wir den Wert der currentTime-Eigenschaft des Audios auf den Wert des Suchreglers. Das change-Ereignis des Reglers ist hier sehr nützlich. Wenn wir das input-Ereignis verwenden, werden verschiedene Teile des Audios in sehr kurzer Zeit abgespielt.

Erinnern Sie sich an unser Szenario von 1 bis 20 Werten. Stellen Sie sich jetzt vor, der Benutzer schiebt den Thumb von 1 auf 20 in sagen wir zwei Sekunden. Das sind 20 Sekunden Audio, die in zwei Sekunden abgespielt werden. Es ist, als würde man Busta Rhymes mit 3-facher Geschwindigkeit hören. Ich würde vorschlagen, dass wir das change-Ereignis verwenden. Der Ton wird erst abgespielt, nachdem der Benutzer mit dem Suchen fertig ist. Das ist es, was ich meine.

seekSlider.addEventListener('change', () => {
  audio.currentTime = seekSlider.value;
});

Damit ist das geklärt, aber während der Ton abgespielt wird, muss etwas getan werden. Das ist es, den Wert des Reglers auf die aktuelle Zeit des Audios zu setzen. Oder den Thumb des Reglers jede Sekunde um einen Tick zu bewegen. Da die Audiolänge und der max-Wert des Reglers gleich sind, erreicht der Thumb das Ende des Reglers, wenn der Ton endet. Nun sollte das timeupdate-Ereignis der HTMLMediaElement-Schnittstelle das geeignete Ereignis dafür sein. Dieses Ereignis wird ausgelöst, wenn sich der Wert der currentTime-Eigenschaft des Mediums aktualisiert, was ungefähr viermal pro Sekunde geschieht. Im Handler für dieses Ereignis könnten wir also den Wert des Reglers auf die aktuelle Zeit des Audios setzen. Das sollte gut funktionieren.

audio.addEventListener('timeupdate', () => {
  seekSlider.value = Math.floor(audio.currentTime);
});

Allerdings gibt es hier einige Dinge zu beachten:

  1. Während das Audio abgespielt wird und der Wert des Suchreglers aktualisiert wird, kann der Benutzer nicht mit dem Regler interagieren. Wenn das Audio pausiert ist, kann der Regler keine Eingaben vom Benutzer empfangen, da er ständig aktualisiert wird.
  2. Im Handler aktualisieren wir den Wert des Reglers, aber sein Input-Ereignis wird nicht ausgelöst. Dies liegt daran, dass das Ereignis nur ausgelöst wird, wenn ein Benutzer den Wert des Reglers im Browser aktualisiert, und nicht, wenn er programmatisch aktualisiert wird.

Betrachten wir das erste Problem.

Um mit dem Regler interagieren zu können, während das Audio abgespielt wird, müssten wir den Prozess der Aktualisierung seines Wertes pausieren, wenn er eine Eingabe erhält. Dann, wenn der Regler den Fokus verliert, setzen wir den Prozess fort. Aber wir haben keinen Zugriff auf diesen Prozess. Mein Hack wäre, die globale Methode requestAnimationFrame() für den Prozess zu verwenden. Aber diesmal werden wir das timeupdate-Ereignis dafür nicht verwenden, weil es immer noch nicht funktionieren würde. Die Animation würde ewig laufen, bis der Ton pausiert ist, und das ist nicht das, was wir wollen. Daher verwenden wir das Klickereignis der Wiedergabe-/Pause-Schaltfläche.

Um die Methode requestAnimationFrame() für diese Funktion zu verwenden, müssen wir folgende Schritte ausführen:

  1. Erstellen Sie eine Funktion, um unsere Anweisung „Update Slider Value“ beizubehalten.
  2. Initialisieren Sie in der zuvor erstellten Funktion eine Variable, um die von der Funktion zurückgegebene Anforderungs-ID zu speichern (die zum Pausieren des Update-Prozesses verwendet wird).
  3. Fügen Sie die Anweisungen im Handler des Klickereignisses der Wiedergabe-/Pause-Schaltfläche hinzu, um den Prozess in den jeweiligen Blöcken zu starten und zu pausieren.

Dies wird im folgenden Ausschnitt veranschaulicht.

let rAF = null;

const whilePlaying = () => {
  seekSlider.value = Math.floor(audio.currentTime);
  rAF = requestAnimationFrame(whilePlaying);
}

playIconContainer.addEventListener('click', () => {
  if(playState === 'play') {
    audio.play();
    playAnimation.playSegments([14, 27], true);
    requestAnimationFrame(whilePlaying);
    playState = 'pause';
  } else {
    audio.pause();
    playAnimation.playSegments([0, 14], true);
    cancelAnimationFrame(rAF);
    playState = 'play';
  }
});

Aber das löst unser Problem nicht genau. Der Prozess wird nur pausiert, wenn der Ton pausiert ist. Wir müssen den Prozess auch pausieren, wenn er ausgeführt wird (d. h. wenn der Ton abgespielt wird), wenn der Benutzer mit dem Regler interagieren möchte. Dann, nachdem der Regler den Fokus verloren hat, wenn der Prozess zuvor im Gange war (d. h. wenn der Ton abgespielt wurde), starten wir den Prozess erneut. Dafür würden wir den Input-Handler des Reglers verwenden, um den Prozess zu pausieren. Um den Prozess wieder zu starten, würden wir das change-Ereignis verwenden, da es ausgelöst wird, nachdem der Benutzer den Thumb verschoben hat. Hier ist die Implementierung:

seekSlider.addEventListener('input', () => {
  currentTimeContainer.textContent = calculateTime(seekSlider.value);
  if(!audio.paused) {
    cancelAnimationFrame(raf);
  }
});

seekSlider.addEventListener('change', () => {
  audio.currentTime = seekSlider.value;
  if(!audio.paused) {
    requestAnimationFrame(whilePlaying);
  }
});

Ich konnte etwas für das zweite Problem finden. Ich habe die Anweisungen in den Input-Event-Handlern des Suchreglers zur Funktion whilePlaying() hinzugefügt. Erinnern Sie sich, dass es zwei Event-Listener für das Input-Ereignis des Reglers gibt: einen für die Präsentation und einen für die Funktionalität. Nachdem die beiden Anweisungen aus den Handlern hinzugefügt wurden, sieht unsere whilePlaying()-Funktion wie folgt aus:

const whilePlaying = () => {
  seekSlider.value = Math.floor(audio.currentTime);
  currentTimeContainer.textContent = calculateTime(seekSlider.value);
  audioPlayerContainer.style.setProperty('--seek-before-width', `${seekSlider.value / seekSlider.max * 100}%`);
  raf = requestAnimationFrame(whilePlaying);
}

Beachten Sie, dass die Anweisung in der vierten Zeile die entsprechende Anweisung des Suchschiebereglers aus der Funktion showRangeProgress() ist, die wir früher im Präsentationsabschnitt erstellt haben.

Jetzt bleiben uns nur noch die Lautstärkeregelungsfunktionen. Puh! Aber bevor wir damit beginnen, hier ist ein Pen, der alles abdeckt, was wir bisher getan haben

Lautstärkeregelung

Für die Lautstärkeregelung verwenden wir den zweiten Schieberegler, #volume-slider. Wenn der Benutzer mit dem Schieberegler interagiert, wird der Wert des Schiebereglers in der Lautstärke des Audios und im zuvor erstellten <output>-Element widergespiegelt.

Die max-Eigenschaft des Schiebereglers hat einen Standardwert von 100. Dies erleichtert die Anzeige seines Wertes im <output>-Element, wenn es aktualisiert wird. Wir könnten dies im Eingabeereignisbehandler des Schiebereglers implementieren. Um dies jedoch für die Lautstärke des Audios zu implementieren, müssen wir einige Berechnungen durchführen.

Die HTMLMediaElement-Schnittstelle stellt eine volume-Eigenschaft bereit, die einen Wert zwischen 0 und 1 zurückgibt, wobei 1 die lauteste Einstellung ist. Das bedeutet, wenn der Benutzer den Wert des Schiebereglers auf 50 setzt, müssten wir die Lautstärke auf 0,5 einstellen. Da 0,5 ein Hundertstel von 50 ist, könnten wir die Lautstärke auf ein Hundertstel des Schiebereglerwerts einstellen.

const volumeSlider = document.getElementById('volume-slider');
const outputContainer = document.getElementById('volume-output');

volumeSlider.addEventListener('input', (e) => {
  const value = e.target.value;

  outputContainer.textContent = value;
  audio.volume = value / 100;
});

Nicht schlecht, oder?

Audio stummschalten

Als Nächstes kommt das Lautsprechersymbol, das geklickt wird, um den Ton stummzuschalten und die Stummschaltung aufzuheben. Um den Ton stummzuschalten, würden wir seine muted-Eigenschaft verwenden, die über HTMLMediaElement ebenfalls als boolescher Typ verfügbar ist. Ihr Standardwert ist false, was bedeutet, dass sie nicht stummgeschaltet ist. Um den Ton stummzuschalten, setzen wir die Eigenschaft auf true. Wenn Sie sich erinnern, haben wir dem Lautsprechersymbol für die Präsentation (die Lottie-Animation) einen Click-Event-Listener hinzugefügt. Um den Ton stummzuschalten und die Stummschaltung aufzuheben, sollten wir die Anweisungen zu den entsprechenden bedingten Blöcken in diesem Handler hinzufügen, wie im Folgenden

const muteIconContainer = document.getElementById('mute-icon');

muteIconContainer.addEventListener('click', () => {
  if(muteState === 'unmute') {
    muteAnimation.playSegments([0, 15], true);
    audio.muted = true;
    muteState = 'mute';
  } else {
    muteAnimation.playSegments([15, 25], true);
    audio.muted = false;
    muteState = 'unmute';
  }
});

Vollständige Demo

Hier ist die vollständige Demo unseres benutzerdefinierten Audioplayers in seiner ganzen Pracht!

Aber bevor wir uns verabschieden, möchte ich Ihnen etwas vorstellen – etwas, das unserem Benutzer Zugriff auf die Medienwiedergabe außerhalb des Browser-Tabs ermöglicht, in dem sich unser benutzerdefinierter Audioplayer befindet.

Erlauben Sie mir, Ihnen vorzustellen, Trommelwirbel, bitte…

Die Media Session API

Grundsätzlich ermöglicht diese API dem Benutzer das Anhalten, Abspielen und/oder Ausführen anderer Medienwiedergabeaktionen, jedoch nicht mit unserem Audioplayer. Abhängig vom Gerät oder Browser initiiert der Benutzer diese Aktionen über den Benachrichtigungsbereich, Mediendrehkreuze oder jede andere Schnittstelle, die von seinem Browser oder Betriebssystem bereitgestellt wird. Ich habe einen weiteren Artikel nur dazu, damit Sie mehr Kontext dazu erhalten.

Der folgende Pen enthält die Implementierung der Media Session API

Wenn Sie diesen Pen auf Ihrem Handy ansehen, werfen Sie einen Blick in den Benachrichtigungsbereich. Wenn Sie Chrome auf Ihrem Computer verwenden, schauen Sie sich das Media Hub an. Wenn Ihre Smartwatch gekoppelt ist, empfehle ich Ihnen, sie anzusehen. Sie könnten auch Ihren Sprachassistenten bitten, einige Aktionen für den Ton auszuführen. Zehn Euro darauf, dass es Sie zum Lächeln bringt. 🤓

Noch etwas…

Wenn Sie einen Audioplayer auf einer Webseite benötigen, besteht eine hohe Wahrscheinlichkeit, dass die Seite andere Inhalte enthält. Deshalb halte ich es für clever, den Audioplayer und den gesamten dafür benötigten Code in eine Webkomponente zu gruppieren. Auf diese Weise verfügt die Webseite über eine Form der Trennung der Verantwortlichkeiten. Ich habe alles, was wir getan haben, in eine Webkomponente übertragen und das Folgende erstellt

Zusammenfassend lässt sich sagen, dass die Möglichkeiten zur Erstellung eines Mediaplayers mit der HTMLMediaElement-Schnittstelle endlos sind. Es gibt so viele verschiedene Eigenschaften und Methoden für verschiedene Funktionen. Dann gibt es noch die Media Session API für ein verbessertes Erlebnis.

Wie lautet das Sprichwort? Mit großer Macht kommt große Verantwortung, oder? Denken Sie an all die verschiedenen Steuerungen, Elemente und Randfälle, die wir für einen letztendlich bescheidenen benutzerdefinierten Audioplayer berücksichtigen mussten. Das zeigt nur, dass Audioplayer mehr sind als nur das Drücken von Play und Pause. Eine ordnungsgemäße Spezifikation funktionaler Anforderungen hilft definitiv bei der Vorausplanung Ihres Codes und spart Ihnen viel Zeit.