# React Audio Player mit Waveform: Scrubbing und Peaks

> Bau einen React Audio Player mit Waveform für Podcasts, Sprachnachrichten und Anruflisten: Spulen per Tastatur, vorberechnete Peaks, keine Layout-Sprünge.

- Autor: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Veröffentlicht: 19. Okt. 2026
- Kategorie: [Komponenten-Guides](https://wingo-ui.com/de/blog/category/components)
- Lesezeit: 9 Min.
- Kanonische URL: https://wingo-ui.com/de/blog/react-audio-player-with-waveform
- Englische Fassung: https://wingo-ui.com/blog/react-audio-player-with-waveform

## Kurz gesagt

Ein React Audio Player mit Waveform braucht Balken, die du anklicken, ziehen und mit den Pfeiltasten bewegen kannst, einen Seek-Slider, den Screenreader als Zeit ansagen, und eine Waveform, die ihre Größe behält, während die echten Peaks laden. Berechne Peaks für lange Dateien auf dem Server vor, denn das Dekodieren im Browser hält die ganze Aufnahme im Speicher. Der Audio Player von Wingo UI bringt eine Podcast-Karte, eine Sprachnachricht für Chats und eine Zeile für Anruflisten mit, alle auf Basis des Hooks useMediaControls.

Du brauchst einen React Audio Player mit Waveform: eine Podcast-Folge auf einer Landing Page, Sprachnachrichten in einem Chat oder aufgezeichnete Anrufe in einem CRM. Das native Element `<audio controls>` spielt die Datei ab, sieht aber in jedem Browser anders aus, zeigt keine Waveform und passt schlecht in eine Chat-Blase. Eine Waveform-Library zeichnet die Balken; die schwierigen Teile bleiben trotzdem bei dir: Scrubbing auf dem Smartphone, Spulen per Tastatur, was ein Screenreader hört und der Sprung im Layout, wenn die echte Waveform eintrifft. Dieser Guide behandelt genau diese Teile mit dem [Audio Player](https://wingo-ui.com/components/audio-player) von Wingo UI und dem Hook [useMediaControls](https://wingo-ui.com/components/use-media-controls), auf dem er aufbaut.

## Was muss ein React Waveform Audio Player können?

Sieben Dinge. Ein React Waveform Audio Player, der im Produktiveinsatz besteht, braucht:

1. **Klicken, Tippen und Ziehen zum Spulen** auf den Balken, und die Zeit folgt dem Finger, während du ziehst.
2. **Spulen per Tastatur** mit den Pfeiltasten, Bild auf und Bild ab, Pos1 und Ende.
3. **Einen Slider, der eine Zeit ansagt.** Screenreader lesen die Position als Text vor, etwa „1:12 of 4:05“, statt einer rohen Sekundenzahl.
4. **Ein stabiles Layout.** Die Waveform hat ab dem ersten Render eine feste Höhe und Platzhalter-Balken, damit nichts springt, wenn die dekodierten Peaks eintreffen.
5. **Peaks, die auch für lange Dateien taugen.** Kurze Clips kannst du im Browser dekodieren; lange brauchen vorberechnete Peaks.
6. **Immer nur einen aktiven Player.** Startet eine Sprachnachricht, pausiert die, die gerade lief.
7. **Geschwindigkeit, Sperrbildschirm und Kopfhörertasten**, denn Podcasts hören viele mit 1,5x und dem Handy in der Tasche.

## Wie schneiden die Waveform-Optionen im Vergleich ab?

Die eigentliche Wahl: eine Waveform-Library, die du in deine eigene Oberfläche einbaust, oder eine fertige Player-Komponente. Die Tabelle stellt je ein Beispiel gegenüber, mit Fakten aus der öffentlichen Doku der Projekte, Stand Oktober 2026.

| Option | Was es ist | Peaks | Player-Oberfläche |
|---|---|---|---|
| [wavesurfer.js](https://wavesurfer.xyz/) mit [@wavesurfer/react](https://www.npmjs.com/package/@wavesurfer/react) | Waveform-Library unter BSD-3-Clause; das React-Paket exportiert `WavesurferPlayer` und `useWavesurfer` | Dekodiert im Browser oder nimmt vorberechnete `peaks` und `duration` | Buttons, Zeit und Layout baust du selbst; Plugins ergänzen Regionen, Zeitleisten und Spektrogramme |
| [Wingo UI Audio Player](https://wingo-ui.com/components/audio-player) | Pro-Komponente als Quellcode, `npx wingo-ui@latest add audio-player` | Dekodiert im Browser, sobald er sichtbar ist, oder nimmt ein `peaks`-Array | Varianten Karte, kompakte Sprachnachricht und minimale Anrufzeile, Geschwindigkeit, Lautstärke, Download, Sperrbildschirm |

wavesurfer.js ist die bessere Wahl, wenn die Waveform selbst das Produkt ist: Bearbeiten, Regionen, Zoomen oder ein Spektrogramm. Für einen Podcast Player in React oder einen Player für Sprachnachrichten ist ein fertiger Player weniger Arbeit: Er hat die Bedienelemente, die Tastaturbelegung und die Lade- und Fehlerzustände schon an Bord.

## Welche Props braucht der Audio Player?

Nur `src`. Alles andere hat einen Default: die Variante `card`, den neutralen Tone, Skip-Buttons für 15 Sekunden, ein Geschwindigkeitsmenü von 0,5x bis 2x und so viele Balken, wie in die Breite passen.

```bash
npx wingo-ui@latest add audio-player
```

```tsx
import { AudioPlayer } from "@/components/ui/audio-player";

export function Episode() {
  return (
    <AudioPlayer
      src="/episodes/12.mp3"
      title="Umsatzsteuer für kleine Unternehmen"
      subtitle="Summit Podcast · Folge 12 · 24. Sep. 2026"
      cover="/episodes/12.jpg"
      downloadable
    />
  );
}
```

Am häufigsten änderst du `variant` (`card`, `compact` oder `minimal`), `tone` oder `color` für die gespielten Balken und den Play-Button, `skipSeconds` (0 blendet die Skip-Buttons aus), `rates` für die Liste der Geschwindigkeiten sowie `barWidth`, `barGap` und `waveformHeight` für dichtere oder höhere Balken. `title`, `subtitle` und `cover` werden während der Wiedergabe außerdem zu den Metadaten auf dem Sperrbildschirm.

Drück auf Play und zieh dann über die Balken. Mit der Maus siehst du beim Hovern die Zeit über dem Cursor; mit der Tastatur springst du per Tab zur Waveform und nutzt die Pfeiltasten.

> Live-Demo (Audio Player): Drück auf Play und zieh dann über die Waveform, um zu spulen. Spring per Tab zu den Balken: Pfeil nach rechts springt 5 Sekunden vor, Bild auf 30. Probier sie unter [Audio Player](https://wingo-ui.com/components/audio-player) aus und installier sie mit `npx wingo-ui@latest add audio-player`.

## Wie vermeide ich Layout-Sprünge, während die Waveform lädt?

Reservier den Platz und füll ihn ab dem ersten Render mit Balken. Die Waveform ist in der Karte 40px hoch und im kompakten Player 28px (`waveformHeight` überschreibt beides), ihre Box ändert also nie die Größe. Solange es keine echten Peaks gibt, zeichnet der Player `placeholderPeaks(src, count)`: stabile, sprachähnliche Balken, die aus der URL abgeleitet werden und auf Server und Client identisch sind. Es gibt also keinen Hydration-Mismatch. Auf dem Server verteilen sich die Balken über die Breite; nach dem Mount zählt ein ResizeObserver, wie viele Balken von je `barWidth + barGap` hineinpassen, und zeichnet neu.

Treffen die dekodierten Peaks ein, überblenden die neuen Balken die Platzhalter. Der gespielte Teil besteht aus denselben Balken im Tone, per `clip-path` aus einem Motion Value zugeschnitten. So löst die Wiedergabe nur einen Repaint aus, statt React bei jedem Frame neu zu rendern. Der Zeittext nutzt Tabellenziffern und wechselt bei Aufnahmen ab einer Stunde in ein Format mit Stunden, damit seine Breite nicht zappelt.

## Soll ich Peaks im Browser dekodieren oder vorberechnen?

Kurze Dateien dekodierst du, lange rechnest du vor. Ohne den Prop `peaks` wartet der Player, bis er höchstens 200px vom Viewport entfernt ist, lädt die Datei, dekodiert sie mit einem `OfflineAudioContext` und behält pro Abschnitt den lautesten Wert (`decodePeaks` cacht das Ergebnis pro URL und Balkenzahl). Für eine Sprachnachricht von 30 Sekunden ist das in Ordnung. Die [Performance-Hinweise von wavesurfer.js](https://wavesurfer.xyz/docs/performance/) erklären die Grenze: Das Dekodieren mit Web Audio hält die ganze Datei als rohes PCM im Speicher, und ein Stereo-Track von 60 Minuten kann auf über 500 MB kommen. Außerdem muss die Datei für das Dekodieren auf deiner Origin liegen oder mit CORS ausgeliefert werden; eine blockierte Datei bleibt bei den Platzhalter-Balken.

Für Podcasts und Anrufaufzeichnungen erzeugst du die Peaks einmal beim Upload. [audiowaveform](https://github.com/bbc/audiowaveform) von der BBC schreibt sie als JSON; dieser Befehl stammt aus dem [Peaks-Guide von wavesurfer.js](https://wavesurfer.xyz/docs/peaks/):

```bash
audiowaveform -i 12.mp3 -o 12.waveform.json --pixels-per-second 20 --bits 8
```

Das Array `data` im JSON enthält pro Kanal abwechselnd Minimum und Maximum ([Datenformat](https://github.com/bbc/audiowaveform/blob/master/doc/DataFormat.md)). Der Player will pro Balken eine Zahl von 0 bis 1, in beliebiger Länge. Fass also jedes Paar zu seinem größeren Absolutwert zusammen, reduzier auf ein paar hundert Abschnitte und normalisier:

```ts
// scripts/peaks.ts: npx tsx scripts/peaks.ts 12.waveform.json public/episodes/12.peaks.json
import { readFileSync, writeFileSync } from "node:fs";

type Waveform = { channels?: number; length: number; data: number[] };

const [input, output] = process.argv.slice(2);
const json = JSON.parse(readFileSync(input, "utf8")) as Waveform;
const channels = json.channels ?? 1;
const buckets = Math.min(400, json.length);
const peaks = new Array<number>(buckets).fill(0);

for (let i = 0; i < json.length; i++) {
  const bucket = Math.floor((i * buckets) / json.length);
  for (let c = 0; c < channels; c++) {
    const at = (i * channels + c) * 2;
    const value = Math.max(Math.abs(json.data[at]), Math.abs(json.data[at + 1]));
    if (value > peaks[bucket]) peaks[bucket] = value;
  }
}

const loudest = peaks.reduce((max, value) => Math.max(max, value), 0) || 1;
writeFileSync(output, JSON.stringify(peaks.map((value) => Math.round((value / loudest) * 1000) / 1000)));
```

Dann liest du die Datei in einer Server Component und gibst sie weiter. Der Player überspringt das Dekodieren und zeigt die echte Waveform schon im Server-HTML:

```tsx
import { readFile } from "node:fs/promises";
import path from "node:path";
import { AudioPlayer } from "@/components/ui/audio-player";

export default async function EpisodePage() {
  const file = path.join(process.cwd(), "public/episodes/12.peaks.json");
  const peaks = JSON.parse(await readFile(file, "utf8")) as number[];

  return <AudioPlayer src="/episodes/12.mp3" peaks={peaks} title="Umsatzsteuer für kleine Unternehmen" />;
}
```

## Wie baue ich einen Player für Sprachnachrichten in React?

Nimm `variant="compact"`: Play, Balken, Zeit und ein Geschwindigkeits-Chip, der bei jedem Tippen zwischen 1x, 1,5x und 2x wechselt, passend für eine Chat-Blase. Leg einen Avatar oder ein Mikrofon-Icon in `leading`. `exclusive` ist standardmäßig an, also pausiert eine startende Sprachnachricht jeden anderen exklusiven Player auf der Seite.

```tsx
import { AudioPlayer } from "@/components/ui/audio-player";
import { Avatar } from "@/components/ui/avatar";

export function VoiceNote({ src, from }: { src: string; from: { name: string; avatar?: string } }) {
  return (
    <div className="max-w-[300px] rounded-[22px] rounded-bl-[6px] bg-surface px-3 py-2.5 shadow-raised">
      <AudioPlayer
        variant="compact"
        src={src}
        leading={<Avatar src={from.avatar} name={from.name} size="sm" />}
        labels={{ play: "Anhören", pause: "Stopp" }}
      />
    </div>
  );
}
```

Der kompakte Player zeigt vor dem Start die Länge und danach die Position. Die andere Hälfte ist das Aufnehmen: Der [Voice Input](https://wingo-ui.com/components/voice-input) nimmt mit MediaRecorder auf und gibt dir den Blob, und der [Guide zum React Speech to Text Input](https://wingo-ui.com/de/blog/react-speech-to-text-input) behandelt Mikrofon, Live-Transkript und Fallback auf den Server.

Eine Falle bei frischen Aufnahmen: Der MediaRecorder von Chrome schreibt WebM-Dateien seit Langem ohne Dauer ([Chromium Issue 642012](https://crbug.com/642012)), deshalb kann das audio-Element `Infinity` melden. Seit Chrome 140 hat eine Aufnahme am Stück die Dauer, eine mit timeslice oder `requestData()`-Aufrufen aber weiterhin nicht ([Addpipe, aktualisiert im Juni 2026](https://blog.addpipe.com/duration-in-webm-videos-produced-by-chrome/)). Der Voice Input nimmt in Abschnitten von einer Sekunde auf, rechne bei seinen Blobs also mit der fehlenden Dauer. Der Audio Player behandelt eine unendliche Dauer als Livestream: Er zeigt ein Badge „Live“ und schaltet das Spulen ab. Korrigier die Dauer in der aufgenommenen Datei, bevor du sie abspielst, oder spiel die hochgeladene Datei ab, sobald der Server sie gespeichert hat.

## Wie zeige ich aufgezeichnete Anrufe in einer Liste?

Nimm `variant="minimal"`: Play, eine dünne Leiste zum Scrubben, die Zeit und optional einen Download-Button, klein genug für eine Tabellenzeile oder einen Activity Feed. Die Waveform fällt hier absichtlich weg, denn dreißig Zeilen voller Balken sind nur Rauschen. `downloadable` ergänzt den Download-Button, und `fileName` benennt die gespeicherte Datei.

```tsx
<AudioPlayer variant="minimal" size="sm" src={call.recordingUrl} downloadable fileName={`Anruf ${call.contact}`} />
```

> Live-Demo (Audio Player): Ein aufgezeichneter Anruf in einer CRM-Zeile: Spiel ihn ab, zieh an der dünnen Leiste zum Spulen und nutz den Download-Button. Probier sie unter [Audio Player](https://wingo-ui.com/components/audio-player) aus und installier sie mit `npx wingo-ui@latest add audio-player`.

Das [React CRM Template](https://wingo-ui.com/de/blog/react-crm-template) ist ein kompletter Screen, in den solche Anrufzeilen gehören.

## Wie mache ich eine Audio-Waveform barrierefrei?

Behandle die Waveform als Seek-Slider. Der Player rendert sie mit `role="slider"`, dem Label „Seek“, `aria-valuemin` und `aria-valuemax` in Sekunden und `aria-valuetext` wie „1:12 of 4:05“. Das ist dieselbe Idee wie im [Beispiel für einen Media Seek Slider](https://www.w3.org/WAI/ARIA/apg/patterns/slider/examples/slider-seek/) des W3C, das Sekunden in Minuten und Sekunden umrechnet, damit niemand eine rohe Zahl hört.

Auf der Waveform spulen die Pfeiltasten links und rechts (oder hoch und runter) 5 Sekunden, Bild auf und Bild ab 30, Pos1 und Ende springen zum Anfang oder Ende, und Leertaste, k oder Enter starten und pausieren. Überall im Player spulen j und l 10 Sekunden, m schaltet stumm, 0 bis 9 springen auf 0% bis 90%, und die spitzen Klammern ändern die Geschwindigkeit. Jeder Button behält am Smartphone ein Touch Target von 44px, der Lautstärke-Slider verschwindet auf Touchscreens, wo die Hardwaretasten das übernehmen, und das Geschwindigkeitsmenü öffnet sich unter 768px als Bottom Sheet. Eine fehlende Datei zeigt eine Meldung mit „Erneut versuchen“ statt eines Buttons, der nichts tut. Alle Texte liegen in `labels` und werden über `DEFAULT_AUDIO_PLAYER_LABELS` gemergt.

## Wie baue ich ein eigenes Layout mit useMediaControls?

Kombinier den Hook mit dem exportierten Part `Waveform`. So wird daraus eine React Waveform Komponente, die du in jedes Layout setzen kannst. `useMediaControls` gibt dir einen `ref` für das `<audio>`-Element, State wie `playing`, `duration` und `second` (ganze Sekunden, höchstens ein Render pro Sekunde), Motion Values für `progress` und `buffered` sowie Aktionen wie `toggle`, `seek`, `seekBy`, `seekToFraction` und `setRate`. `handleMediaKey` gibt jedem Root-Element dieselbe Tastaturbelegung.

```tsx
"use client";

import { Waveform } from "@/components/ui/audio-player";
import { formatMediaTime, handleMediaKey, useMediaControls } from "@/hooks/use-media-controls";

export function Clip({ src, peaks }: { src: string; peaks: number[] }) {
  // ref destrukturieren, damit der react compiler lint nicht das ganze objekt als ref behandelt
  const { ref, ...media } = useMediaControls({ exclusive: true });

  return (
    <div onKeyDown={(event) => handleMediaKey(event, media)} className="flex items-center gap-3">
      <audio ref={ref} src={src} preload="metadata" />
      <button type="button" onClick={media.toggle} className="h-11 rounded-full px-4">
        {media.playing ? "Pause" : "Abspielen"}
      </button>
      <Waveform
        peaks={peaks}
        progress={media.progress}
        onSeek={media.seekToFraction}
        formatBubble={(fraction) => formatMediaTime(fraction * media.duration)}
        aria-label="Spulen"
        aria-valuetext={`${formatMediaTime(media.second)} von ${formatMediaTime(media.duration)}`}
        className="flex-1"
      />
    </div>
  );
}
```

`aria-label` und `aria-valuetext` gibst du hier selbst mit: Mit einem Motion Value für `progress` kann `Waveform` keinen Zahlenwert berechnen. Musst du nur auf die Wiedergabe reagieren, hat `AudioPlayer` den Prop `setApi`, der dir dieselben Steuerungen ohne ref gibt, etwa um den aktuellen Satz eines Transkripts hervorzuheben und zu spulen, wenn jemand auf eine Zeile tippt.

> Live-Demo (useMediaControls): Spiel den Podcast und die Sprachnachrichten ab: Die Leiste bewegt sich in jedem Frame, die Zeit ändert sich einmal pro Sekunde. Mit exclusive pausiert eine startende Sprachnachricht die andere. Probier sie unter [useMediaControls](https://wingo-ui.com/components/use-media-controls) aus und installier sie mit `npx wingo-ui@latest add use-media-controls`.

## Was sind die typischen Fallstricke?

- **Eine von CORS blockierte Datei.** Zum Dekodieren muss `fetch` die Bytes lesen. Dateien auf einer anderen Domain ohne CORS-Header spielen problemlos ab, behalten aber die Platzhalter-Balken. Übergib `peaks` oder liefere die Datei mit CORS aus.
- **Lange Aufnahmen dekodieren.** Rechne Peaks für alles vor, was länger als ein paar Minuten ist, sonst zahlt der Tab für das ganze PCM im Speicher.
- **`preload="none"` ohne Dauer.** Der Player zeigt `--:--` bis zum ersten Abspielen. Bleib bei `metadata`, außer du renderst Hunderte Zeilen.
- **Zwei Player, die gleichzeitig reden.** Lass `exclusive` in Chat-Verläufen und Folgenlisten an.
- **Frische WebM-Blobs in Chrome.** Aufnahmen in Abschnitten haben weiterhin keine Dauer. Korrigier sie oder spiel die hochgeladene Datei ab, wie oben beschrieben.

## Wie füge ich ihn meinem Projekt hinzu?

Führ `npx wingo-ui@latest add audio-player` aus. Die CLI kopiert `audio-player.tsx` nach `components/ui` und `use-media-controls.ts` nach `hooks`, ergänzt die Items, auf denen der Player aufbaut (Button, Dropdown Menu, Popover, Slider, Spinner, Tooltip und ein paar Helfer), und installiert die npm-Pakete, die sie brauchen, etwa `motion` und `lucide-react`. Der Audio Player, der Hook und der Voice Input sind Pro-Items: Pro kostet 8 $ im Monat, 80 $ im Jahr oder einmalig 150 $, Stand Oktober 2026 ([Preise](https://wingo-ui.com/de/pricing)). Weitere Deep Dives zu einzelnen Komponenten findest du in der [Kategorie Komponenten-Guides](https://wingo-ui.com/de/blog/category/components), und der [Guide zu React Form Components](https://wingo-ui.com/de/blog/react-form-components-guide) wendet dieselben Regeln für Tastatur, Touch und Screenreader auf jedes Formularelement an.

## Komponenten in diesem Artikel

- [Audio Player](https://wingo-ui.com/components/audio-player) (Pro): A React audio player with a waveform you can see and scrub, as a podcast card, a chat voice message or a thin call-log bar. Installation: `npx wingo-ui@latest add audio-player`
- [Voice Input](https://wingo-ui.com/components/voice-input) (Pro): A React voice input: a mic that grows into a recording pill with a live waveform and transcript, a full-width bar, or a voice orb. Installation: `npx wingo-ui@latest add voice-input`
- [useMediaControls](https://wingo-ui.com/components/use-media-controls) (Pro): A React hook for custom audio and video players: play, seek, volume, speed, buffering and a smooth playhead from one media element. Installation: `npx wingo-ui@latest add use-media-controls`

## Häufige Fragen

### Wie zeige ich in React eine Waveform für eine Audiodatei an?

Entweder dekodierst du die Datei im Browser mit der Web Audio API und nimmst pro Abschnitt den lautesten Wert, oder du berechnest die Peaks auf dem Server vor, etwa mit audiowaveform von der BBC, und übergibst sie als Array. Für kurze Sprachnachrichten reicht das Dekodieren im Browser; für Podcasts und lange Anrufe rechnest du vor.

### Ist wavesurfer.js der beste Weg zu einer Waveform in React?

wavesurfer.js ist eine ausgereifte Library unter BSD-3-Clause mit einem offiziellen React-Paket, @wavesurfer/react, das eine Komponente WavesurferPlayer und einen Hook useWavesurfer exportiert. Die Library passt gut, wenn du Regionen, Spektrogramme oder andere Plugins brauchst. Willst du eine fertige Player-Oberfläche mit Layouts für Chat und Anruflisten als Quellcode, den du ändern kannst, passt eine Komponente wie der Audio Player von Wingo UI besser.

### Wie mache ich eine Audio-Waveform per Tastatur bedienbar?

Gib der Waveform role slider, ein Label wie Seek und aria-valuetext mit der Position als Text, zum Beispiel 1:12 of 4:05, damit ein Screenreader keine rohe Sekundenzahl vorliest. Die Pfeiltasten sollten ein kleines Stück springen, Bild auf und Bild ab ein größeres, wie im Beispiel für einen Media Seek Slider des W3C.

### Warum hat meine aufgenommene WebM-Sprachnachricht in Chrome keine Dauer?

Der MediaRecorder von Chrome schreibt WebM-Dateien seit Langem ohne Angabe der Dauer (Chromium Issue 642012), deshalb kann das audio-Element Infinity melden. Chrome 140 trägt die Dauer bei Aufnahmen am Stück ein, Aufnahmen mit timeslice oder requestData() haben sie aber weiterhin nicht. Korrigier also die Dauer in der Datei oder spiel den gespeicherten Upload statt des frischen Blobs ab.

### Ist der Audio Player von Wingo UI kostenlos?

Nein. Der Audio Player, der Hook useMediaControls und der Voice Input sind Pro-Items. Einige Bausteine, auf denen sie aufbauen, etwa Button, Tooltip, Popover, Dropdown Menu und Spinner, sind kostenlos; der Slider ist Pro.

---

Quelle: https://wingo-ui.com/de/blog/react-audio-player-with-waveform
