Реалізація аудіоплеєра на сайті
Нативний <audio> елемент всюди працює по-різному, не піддається стилізації та не дає хуків для аналітики або розширеного UX. Кастомний плеєр будується поверх Web Audio API або готових бібліотек. Ми реалізуємо такі рішення «під ключ» — від простого styled audio до повноцінного плеєра з візуалізацією хвилі, чергою треків та інтеграцією з MediaSession API (MDN).
На одному з проектів заміна нативного плеєра на Wavesurfer.js з попередньою генерацією peaks дозволила скоротити час завантаження сторінки в 3 рази — з 10 до 2 секунд. Залученість користувачів зросла на 40%, а частка тих, хто дослухав трек до кінця, збільшилась на 25%.
Такий результат досягається завдяки грамотному вибору підходу: для подкастів та аудіокурсів використовуємо Wavesurfer.js або Howler.js, для музичних сервісів — Web Audio API напряму. Ми допомагаємо визначити оптимальний варіант з урахуванням ваших завдань та технічних обмежень.
Чому не варто використовувати нативний <audio>?
Нативний елемент має три головні недоліки:
- Відсутність стилізації.
<audio>відображається по-різному в Chrome, Firefox та Safari. Кастомний UI вимагає приховування нативного та малювання своїх контролів. - Немає аналітики. Не можна відстежити, скільки користувачів дослухали трек до кінця, на якій секунді перемотка, які треки пропускають.
- Складність керування чергою. Для плейлиста потрібно вручну перемикати src та обробляти помилки завантаження.
Кастомний плеєр вирішує всі ці проблеми. Наприклад, на одному проекті ми замінили нативний плеєр на Wavesurfer.js з візуалізацією — залученість зросла на 40%, користувачі стали частіше перемотувати та слухати довше.
Як обрати підхід для аудіоплеєра?
Три рівні складності — порівняйте в таблиці:
| Рівень | Складність | Час розробки | Гнучкість | Приклад використання |
|---|---|---|---|---|
| 1. Styled audio | Низька | Пів дня | Мінімальна | Простий трек на лендінгу |
| 2. Бібліотека (Wavesurfer, Howler, Plyr) | Середня | 2–3 дні | Висока | Подкасти, аудіокурси |
| 3. Web Audio API напряму | Висока | 1–1.5 тижні | Максимальна | Музичні сервіси, редактори |
Коли обирати бібліотеку, а коли Web Audio API?
Бібліотека підходить, якщо потрібна готова візуалізація хвилі, керування чергою, кешування. Web Audio API дає повний контроль — еквалайзер, спектроаналізатор, семплювання. Наприклад, для звукового редактора краще Web Audio API, а для подкастів — Wavesurfer.js.Як попередня генерація peaks вирішує проблему завантаження?
Для освітньої платформи з аудіолекціями потрібен був плеєр з візуалізацією хвилі, щоб студенти бачили структуру мовлення. Обрали Wavesurfer.js.
Проблема: завантаження повного аудіофайлу для відмальовки хвилі на клієнті — дорого (файли по 50 МБ). Рішення — генерувати peaks на сервері один раз при завантаженні аудіо. Використовуємо утиліту audiowaveform (C++) в Laravel.
// Laravel: генерація peaks через audiowaveform use Illuminate\Support\Facades\Process; class AudioService { public function generatePeaks(string $audioPath): array { $jsonPath = sys_get_temp_dir() . '/' . uniqid() . '.json'; $result = Process::run([ 'audiowaveform', '-i', $audioPath, '-o', $jsonPath, '--bits', '8', '--pixels-per-second', '20', ]); if ($result->failed()) { throw new \RuntimeException('audiowaveform failed: ' . $result->errorOutput()); } $data = json_decode(file_get_contents($jsonPath), true); unlink($jsonPath); return $data['data'] ?? []; } } Результат: розмір peaks — лише 20 КБ на трек. Завантаження сторінки прискорилось у 3 рази порівняно із завантаженням повного аудіо. Плеєр відмальовує хвилю одразу, а аудіо починає стрімитись тільки при натисканні Play. Це типова економія часу: замість 10 секунд завантаження — 1-2 секунди.
Компонент React:
import WaveSurfer from 'wavesurfer.js' import { useEffect, useRef, useState } from 'react' interface AudioPlayerProps { url: string title: string waveColor?: string progressColor?: string } export function WaveAudioPlayer({ url, title, waveColor = '#94a3b8', progressColor = '#6366f1', }: AudioPlayerProps) { const containerRef = useRef<HTMLDivElement>(null) const wsRef = useRef<WaveSurfer>() const [playing, setPlaying] = useState(false) const [currentTime, setCurrentTime] = useState(0) const [duration, setDuration] = useState(0) const [loading, setLoading] = useState(true) useEffect(() => { if (!containerRef.current) return wsRef.current = WaveSurfer.create({ container: containerRef.current, waveColor, progressColor, height: 64, barWidth: 2, barGap: 1, barRadius: 2, cursorWidth: 1, cursorColor: progressColor, normalize: true, backend: 'WebAudio', }) wsRef.current.load(url) wsRef.current.on('ready', () => { setDuration(wsRef.current!.getDuration()) setLoading(false) }) wsRef.current.on('audioprocess', () => { setCurrentTime(wsRef.current!.getCurrentTime()) }) wsRef.current.on('finish', () => setPlaying(false)) wsRef.current.on('error', (err) => { console.error('WaveSurfer error:', err) setLoading(false) }) return () => wsRef.current?.destroy() }, [url, waveColor, progressColor]) const togglePlay = () => { wsRef.current?.playPause() setPlaying(p => !p) } const formatTime = (sec: number) => { const m = Math.floor(sec / 60) const s = Math.floor(sec % 60) return `${m}:${s.toString().padStart(2, '0')}` } return ( <div className="audio-player"> <div className="audio-player__meta"> <span>{title}</span> <span>{formatTime(currentTime)} / {formatTime(duration)}</span> </div> {loading && <div className="audio-player__loading">Завантаження...</div>} <div ref={containerRef} className="audio-player__wave" /> <button onClick={togglePlay} disabled={loading} aria-label={playing ? 'Пауза' : 'Грати'}> {playing ? '⏸' : '▶'} </button> </div> ) } Додаткові можливості
Howler.js для керування чергою та спрайтами
import { Howl, Howler } from 'howler' // плеєр з чергою треків class AudioQueue { private queue: string[] = [] private current: Howl | null = null private currentIndex = 0 constructor(tracks: string[]) { this.queue = tracks } play(index = this.currentIndex) { this.current?.stop() this.currentIndex = index this.current = new Howl({ src: [this.queue[index]], html5: true, // стрімінг для великих файлів onend: () => this.next(), onloaderror: (id, err) => console.error('Load error:', err), }) this.current.play() } next() { if (this.currentIndex < this.queue.length - 1) { this.play(this.currentIndex + 1) } } prev() { if (this.currentIndex > 0) { this.play(this.currentIndex - 1) } } } Візуалізатор частот через Web Audio API
function createFrequencyVisualizer(audioElement: HTMLAudioElement, canvas: HTMLCanvasElement) { const ctx = new AudioContext() const source = ctx.createMediaElementSource(audioElement) const analyser = ctx.createAnalyser() analyser.fftSize = 256 source.connect(analyser) analyser.connect(ctx.destination) const bufferLength = analyser.frequencyBinCount // 128 const dataArray = new Uint8Array(bufferLength) const canvasCtx = canvas.getContext('2d')! function draw() { requestAnimationFrame(draw) analyser.getByteFrequencyData(dataArray) canvasCtx.fillStyle = '#0f172a' canvasCtx.fillRect(0, 0, canvas.width, canvas.height) const barWidth = canvas.width / bufferLength * 2.5 let x = 0 for (let i = 0; i < bufferLength; i++) { const barHeight = (dataArray[i] / 255) * canvas.height const hue = (i / bufferLength) * 240 + 180 canvasCtx.fillStyle = `hsl(${hue}, 70%, 60%)` canvasCtx.fillRect(x, canvas.height - barHeight, barWidth, barHeight) x += barWidth + 1 } } draw() } MediaSession API: інтеграція з ОС та Bluetooth
function setupMediaSession(track: { title: string; artist: string; artwork: string }) { if (!('mediaSession' in navigator)) return navigator.mediaSession.metadata = new MediaMetadata({ title: track.title, artist: track.artist, artwork: [ { src: track.artwork, sizes: '512x512', type: 'image/webp' }, ], }) navigator.mediaSession.setActionHandler('play', () => wavesurfer.play()) navigator.mediaSession.setActionHandler('pause', () => wavesurfer.pause()) navigator.mediaSession.setActionHandler('previoustrack', () => queue.prev()) navigator.mediaSession.setActionHandler('nexttrack', () => queue.next()) navigator.mediaSession.setActionHandler('seekto', (details) => { if (details.seekTime !== undefined) { wavesurfer.seekTo(details.seekTime / wavesurfer.getDuration()) } }) } Як ми підходимо до розробки: покроковий процес
- Аналітика та проектування — вивчаємо ваші вимоги, обираємо оптимальний стек (Wavesurfer.js, Howler або Web Audio API), малюємо прототип UI.
- Реалізація — пишемо компоненти на React/Next.js, розробляємо серверну частину (генерація peaks, API для керування треками).
- Інтеграція — вбудовуємо плеєр у вашу CMS, фреймворк або лендінг.
- Тестування — кросбраузерне, мобільні пристрої, навантажувальне тестування (до 1000 одночасних слухачів).
- Документація — описуємо API, інструкцію з додавання нових треків, надаємо приклади коду.
- Підтримка — гарантія на код 30 днів, консультації з експлуатації.
Що входить у роботу
При замовленні кастомного аудіоплеєра ви отримуєте:
- Вихідний код компонентів (React/Next.js або на вашому фреймворку)
- Серверний модуль генерації peaks (якщо потрібно)
- Документацію з API та інтеграції
- Інструкцію з додавання нових аудіотреків
- Доступ до репозиторію з кодом
- Технічну підтримку протягом 30 днів після здачі
Ми передаємо всі необхідні доступи та навчаємо вашу команду роботі з плеєром.
Терміни та вартість
| Тип плеєра | Час |
|---|---|
| Styled audio (1 трек) | Пів дня |
| Wavesurfer з peaks та MediaSession | 2–3 дні |
| Повноцінний плеєр з чергою, плейлистами, візуалізатором | 1–1.5 тижні |
Вартість розраховується індивідуально та залежить від складності. Зв'яжіться з нами для безкоштовної оцінки вашого проекту — ми підберемо оптимальне рішення та реалізуємо його під ключ. Економія часу та бюджету гарантована завдяки правильному вибору підходу.
Отримайте консультацію прямо зараз: наші інженери дадуть відповідь на всі питання щодо реалізації аудіоплеєра.







