fix: replace new Audio() with howler.js for reliable sound playback + error logging
Some checks failed
Deploy / build-and-deploy (push) Has been cancelled

This commit is contained in:
2026-06-15 01:35:08 +03:00
parent 19d7a161c7
commit 05c4f19109
3 changed files with 49 additions and 26 deletions

View File

@@ -8,6 +8,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from ".
import { SOUND_CATEGORIES, EMOJIS } from "@/lib/bingo-data"; import { SOUND_CATEGORIES, EMOJIS } from "@/lib/bingo-data";
import { Badge } from "./ui/badge"; import { Badge } from "./ui/badge";
import { ResourceBrowser, DragData } from "./ResourceBrowser"; import { ResourceBrowser, DragData } from "./ResourceBrowser";
import { playSoundOnce } from "@/lib/sounds";
type Item = { type Item = {
id: string; id: string;
@@ -200,10 +201,6 @@ export function ItemEditor({ campaign }: { campaign: Campaign }) {
setUploading(false); setUploading(false);
}; };
const playSoundOnce = (url: string) => {
try { const a = new Audio(url); a.preload = "auto"; a.volume = 0.4; a.play().catch(() => {}); } catch {}
};
// ─── Drag & Drop handlers ─────────────────────────────────────── // ─── Drag & Drop handlers ───────────────────────────────────────
const handleDragOver = useCallback((e: React.DragEvent, idx: number) => { const handleDragOver = useCallback((e: React.DragEvent, idx: number) => {

View File

@@ -2,7 +2,7 @@
import { useState, useEffect, useCallback, useRef } from "react"; import { useState, useEffect, useCallback, useRef } from "react";
import { useDropzone } from "react-dropzone"; import { useDropzone } from "react-dropzone";
import { Howl } from "howler"; import { playSoundOnce } from "@/lib/sounds";
// ─── Types ────────────────────────────────────────────────────────── // ─── Types ──────────────────────────────────────────────────────────
@@ -33,19 +33,6 @@ export type DragData =
| { type: "image"; url: string; originalName: string } | { type: "image"; url: string; originalName: string }
| { type: "emoji"; emoji: string }; | { type: "emoji"; emoji: string };
// ─── Howl helper ────────────────────────────────────────────────────
const howlCache = new Map<string, Howl>();
function playPreview(url: string) {
let h = howlCache.get(url);
if (!h) {
h = new Howl({ src: [url], format: ["mp3", "ogg"], volume: 0.4, preload: true });
howlCache.set(url, h);
}
h.play();
}
// ─── Emoji Picker (inline, searchable) ────────────────────────────── // ─── Emoji Picker (inline, searchable) ──────────────────────────────
const EMOJI_LIST = [ const EMOJI_LIST = [
@@ -211,7 +198,7 @@ function SoundsTab() {
label={s.originalName} label={s.originalName}
duration={s.duration} duration={s.duration}
url={s.url} url={s.url}
onPlay={() => playPreview(s.url)} onPlay={() => playSoundOnce(s.url)}
onDelete={() => deleteSound(filename)} onDelete={() => deleteSound(filename)}
/> />
); );
@@ -244,7 +231,7 @@ function SoundsTab() {
key={r.id} key={r.id}
name={r.name} name={r.name}
duration={r.duration} duration={r.duration}
onPreview={() => r.previewUrl && playPreview(r.previewUrl)} onPreview={() => r.previewUrl && playSoundOnce(r.previewUrl)}
onImport={() => importSound(r)} onImport={() => importSound(r)}
/> />
))} ))}

View File

@@ -1,3 +1,5 @@
import { Howl } from "howler";
let audioCtx: AudioContext | null = null; let audioCtx: AudioContext | null = null;
function getCtx(): AudioContext { function getCtx(): AudioContext {
@@ -107,13 +109,50 @@ export function playChaosRiser() {
}); });
} }
// ─── Howl-based URL playback ──────────────────────────────────────
const howlCache = new Map<string, Howl>();
/**
* Play a sound file URL (MP3/OGG) using howler.js.
* Howler handles autoplay policy, cross-browser codec support,
* and AudioContext resume better than raw new Audio().
*/
export function playSoundUrl(url: string) { export function playSoundUrl(url: string) {
try { let h = howlCache.get(url);
const audio = new Audio(url); if (!h) {
audio.preload = "auto"; h = new Howl({
audio.volume = 0.4; src: [url],
audio.play().catch(() => {}); format: url.endsWith(".mp3") ? ["mp3"] : url.endsWith(".ogg") ? ["ogg"] : ["mp3", "ogg"],
} catch {} volume: 0.4,
onloaderror: (_id: number, err: unknown) => {
console.error("[BaraBingo] Howl load error:", url, err);
},
onplayerror: (_id: number, err: unknown) => {
console.error("[BaraBingo] Howl play error:", url, err);
},
});
howlCache.set(url, h);
}
h.play();
}
/**
* One-shot play of a sound URL. Same as playSoundUrl but always creates a fresh instance.
* Useful for preview/editor buttons where you want it to play every click.
*/
export function playSoundOnce(url: string) {
new Howl({
src: [url],
format: url.endsWith(".mp3") ? ["mp3"] : url.endsWith(".ogg") ? ["ogg"] : ["mp3", "ogg"],
volume: 0.4,
onloaderror: (_id: number, err: unknown) => {
console.error("[BaraBingo] Howl load error:", url, err);
},
onplayerror: (_id: number, err: unknown) => {
console.error("[BaraBingo] Howl play error:", url, err);
},
}).play();
} }
export function playSound(category: string, soundUrl?: string | null) { export function playSound(category: string, soundUrl?: string | null) {