Navigácia
AI

PWA a Web Audio API: offline prehrávač a pasce prehrávania na pozadí

30 októbra, 2026 techy.sk

Postaviť offline prehrávač zvuku ako PWA sa dá. Service Worker však udrží v pamäti súbory, nie samotné prehrávanie. Či zvuk pobeží na uzamknutej obrazovke, rozhoduje prehliadač a operačný systém. Tento návod ukazuje, čo funguje spoľahlivo, kde sú pasce a prečo je výber medzi Web Audio API a elementom audio dôležitejší než samotné cachovanie.

Najprv oprava predstavy

Často sa tvrdí, že PWA „zachráni“ webovú aplikáciu pred zabitím na pozadí. Nie je to celkom tak. Service Worker rieši offline prístup a rýchle načítanie, ale prehrávanie zvuku a jeho prežitie pri zamknutí obrazovky závisí od správania prehliadača:

  • Pri bežnom elemente audio a správne nastavenom Media Session API prehrávanie na pozadí väčšinou funguje, hoci s rozdielmi medzi systémami.
  • Pri čistom AudioContext je situácia horšia. Na iOS sa kontext po prechode na pozadie rád pozastaví a vývojári hlásia aj prípady, keď po návrate zostane ticho.
  • Zvlášť problematické sú PWA pridané na plochu iOS. V jeseni 2025 boli hlásené chyby po aktualizácii na iOS 26, pri ktorých sa Web Audio v takejto aplikácii správalo nespoľahlivo. Stav sa môže medzitým zmeniť, preto si správanie vždy over na reálnom zariadení.

Preto navrhujeme architektúru, ktorá minimalizuje závislosť od AudioContext pri dlhom prehrávaní.

Architektúra

  • Service Worker cachuje aplikáciu aj zvukové súbory a vie ich servírovať offline.
  • Element audio prehráva dlhé nahrávky (ambient, hudbu). Je to najspoľahlivejšia cesta pre prehrávanie na pozadí.
  • Media Session API poskytuje ovládanie na zamknutej obrazovke a v systémových oznámeniach.
  • Web Audio API použi na syntézu a efekty, napríklad pre binaurálne tóny, ktoré nepotrebujú žiadny súbor.

Web App Manifest

{
  "name": "Offline prehrávač",
  "short_name": "Prehrávač",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#111111",
  "theme_color": "#111111",
  "icons": [
    { "src": "/icons/192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icons/512.png", "sizes": "512x512", "type": "image/png" }
  ]
}

Service Worker: cachovanie aj Range požiadavky

Prehliadače pri zvuku a videu posielajú požiadavky s hlavičkou Range, aby mohli preskakovať v súbore. Cache API ale neumožňuje uložiť čiastočnú odpoveď (206) a cache.match sám rozsah nerieši. Najmä Safari rozsahy pri médiách vyžaduje. Service Worker ich preto musí zvládnuť sám:

const CACHE = "audio-v1";
const SHELL = ["/", "/app.js", "/styles.css", "/manifest.webmanifest"];

self.addEventListener("install", (event) => {
  event.waitUntil(caches.open(CACHE).then((c) => c.addAll(SHELL)));
  self.skipWaiting();
});

self.addEventListener("activate", (event) => {
  event.waitUntil(
    caches.keys()
      .then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))))
      .then(() => self.clients.claim())
  );
});

self.addEventListener("fetch", (event) => {
  const url = new URL(event.request.url);
  if (url.pathname.startsWith("/audio/")) {
    event.respondWith(serveAudio(event.request));
  } else {
    event.respondWith(caches.match(event.request).then((r) => r || fetch(event.request)));
  }
});

async function serveAudio(request) {
  const cache = await caches.open(CACHE);
  const cached = await cache.match(request.url);
  if (!cached) return fetch(request);

  const range = request.headers.get("range");
  if (!range) return cached;

  const blob = await cached.blob();
  const size = blob.size;
  const m = /bytes=(d*)-(d*)/.exec(range);
  let start = m[1] ? parseInt(m[1], 10) : size - parseInt(m[2], 10);
  let end = m[1] && m[2] ? parseInt(m[2], 10) : size - 1;
  end = Math.min(end, size - 1);

  return new Response(blob.slice(start, end + 1), {
    status: 206,
    statusText: "Partial Content",
    headers: {
      "Content-Type": cached.headers.get("Content-Type") || "audio/mpeg",
      "Content-Range": `bytes ${start}-${end}/${size}`,
      "Content-Length": String(end - start + 1),
      "Accept-Ranges": "bytes",
    },
  });
}

Sťahovanie súborov na offline použitie

Súbor sťahuj ako úplnú odpoveď (bez hlavičky Range) a ulož ho do cache. Požiadaj aj o trvalé úložisko, aby ho prehliadač pri nedostatku miesta nezmazal:

async function saveOffline(url) {
  const cache = await caches.open("audio-v1");
  const res = await fetch(url);
  if (!res.ok) throw new Error("Sťahovanie zlyhalo: " + res.status);
  await cache.put(url, res);
  if (navigator.storage?.persist) await navigator.storage.persist();
}

Kvóta úložiska sa líši podľa prehliadača a zariadenia. Pred sťahovaním veľkých súborov zavolaj navigator.storage.estimate() a informuj používateľa o zostávajúcom mieste.

Prehrávanie na pozadí cez element audio a Media Session

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

async function playAmbient() {
  audio.src = "/audio/ambient.mp3";
  audio.loop = true;
  await audio.play(); // musí byť spustené z gesta používateľa

  navigator.mediaSession.metadata = new MediaMetadata({
    title: "Ambient",
    artist: "Moja aplikácia",
    artwork: [{ src: "/icons/512.png", sizes: "512x512", type: "image/png" }],
  });
  navigator.mediaSession.setActionHandler("play", () => audio.play());
  navigator.mediaSession.setActionHandler("pause", () => audio.pause());
}

Pozor na pauzy. Na iOS boli popísané prípady, keď po dlhšej pauze na pozadí PWA prestalo reagovať na tlačidlá v zamknutej obrazovke, kým sa aplikácia nevrátila do popredia. Test na skutočnom zariadení je preto nevyhnutný.

Binaurálne tóny bez súborov

Binaurálny efekt vzniká, keď do ľavého a pravého ucha ide mierne odlišná frekvencia. Na to netreba žiadny nahraný súbor, stačia dva oscilátory. Prehrávať sa to musí cez slúchadlá. Vedecké dôkazy o účinkoch binaurálnych tónov na náladu alebo sústredenie sú zmiešané, preto ich v aplikácii neprezentuj ako liečbu.

const ctx = new AudioContext();

function startBinaural(base = 200, beat = 10) {
  const left = ctx.createOscillator();
  const right = ctx.createOscillator();
  left.frequency.value = base;
  right.frequency.value = base + beat;

  const merger = ctx.createChannelMerger(2);
  const gain = ctx.createGain();
  gain.gain.value = 0.15;

  left.connect(merger, 0, 0);
  right.connect(merger, 0, 1);
  merger.connect(gain).connect(ctx.destination);

  left.start();
  right.start();
  return () => { left.stop(); right.stop(); };
}

// Kontext sa spúšťa len po geste používateľa
button.addEventListener("click", async () => {
  await ctx.resume();
  stop = startBinaural();
});

// Pokus o obnovenie po návrate do popredia
document.addEventListener("visibilitychange", async () => {
  if (document.visibilityState === "visible" && ctx.state !== "running") {
    try { await ctx.resume(); } catch (e) { /* kontext sa nepodarilo obnoviť */ }
  }
});

Ak sa kontext po návrate nepodarí obnoviť, najspoľahlivejšie je zobraziť používateľovi tlačidlo „Pokračovať“, ktoré vytvorí kontext nanovo po jeho geste.

Pamäť pri dlhom prehrávaní

Metóda decodeAudioData rozbalí celý súbor do nekomprimovanej podoby. Desaťminútová stereo nahrávka pri 44,1 kHz a 32-bitových číslach zaberie približne 212 MB. Na mobile to môže stačiť na zabitie aplikácie. Preto:

  • dlhé nahrávky prehrávaj cez element audio, ktorý ich streamuje,
  • cez decodeAudioData načítavaj len krátke slučky a efekty,
  • nepoužívaj naraz viac inštancií AudioContext, stačí jedna,
  • nepotrebné uzly odpájaj a zdroje po použití uvoľni.

Pozor aj na kombináciu. Ak element audio prepojíš cez createMediaElementSource, začne sa na prehrávanie na pozadí vzťahovať aj správanie AudioContext. Ak ho nepotrebuješ, nechaj element samostatne.

Wake Lock tu nepomôže

Screen Wake Lock API drží obrazovku zapnutú. Pre prehrávanie na uzamknutej obrazovke je to opačné než chceš, pretože zbytočne vybíja batériu. Spoľahlivé prehrávanie vzniká správnym použitím elementu audio a Media Session, nie udržiavaním obrazovky.

Kontrolný zoznam pred nasadením

  • Otestuj zamknutú obrazovku, prepnutie aplikácií a návrat po niekoľkých minútach.
  • Otestuj aj PWA pridanú na plochu iOS, nielen záložku v prehliadači.
  • Otestuj režim v lietadle a aplikáciu po reštarte zariadenia.
  • Skontroluj správanie pri plnom úložisku a pri ukončení sťahovania.
  • Ponúkni bezpečné riešenie, ak sa zvuk preruší, napríklad tlačidlo na obnovenie.

Čo si z toho odniesť

PWA a Service Worker dajú webovej aplikácii offline schopnosti, ale samy nezaručia prehrávanie na pozadí. Najrozumnejší je hybridný prístup: dlhé nahrávky cez element audio, syntézu a efekty cez Web Audio API a dôsledné testovanie na reálnych zariadeniach. Ak potrebuješ stopercentnú spoľahlivosť na iOS, natívna aplikácia zostáva bezpečnejšou voľbou.

Zdieľať článok

Mohlo by vás zaujímať