feat: Integrate Howler.js for audio playback and remove native audio elements
All checks were successful
Build and Push Docker Image / docker (push) Successful in 9s

This commit is contained in:
2025-10-19 22:55:49 +02:00
parent 1dbae8b62b
commit 18d14b097d
11 changed files with 506 additions and 379 deletions

View File

@@ -1,16 +1,15 @@
import {
$answerResult,
$audio,
$guessArtist,
$guessTitle,
$npArtist,
$npTitle,
$npYear,
} from './dom.js';
import { state } from './state.js';
import { cacheLastRoomId, cacheSessionId, sendMsg } from './ws.js';
import { renderRoom } from './render.js';
import { applySync } from './audio.js';
} from "./dom.js";
import { state } from "./state.js";
import { cacheLastRoomId, cacheSessionId, sendMsg } from "./ws.js";
import { renderRoom } from "./render.js";
import { applySync, loadTrack, getSound } from "./audio.js";
function updatePlayerIdFromRoom(r) {
try {
@@ -19,7 +18,7 @@ function updatePlayerIdFromRoom(r) {
if (only && only.id && only.id !== state.playerId) {
state.playerId = only.id;
try {
localStorage.setItem('playerId', only.id);
localStorage.setItem("playerId", only.id);
} catch {}
}
}
@@ -27,7 +26,7 @@ function updatePlayerIdFromRoom(r) {
}
function shortName(id) {
if (!id) return '-';
if (!id) return "-";
const p = state.room?.players.find((x) => x.id === id);
return p ? p.name : id.slice(0, 4);
}
@@ -35,15 +34,15 @@ function shortName(id) {
export function handleConnected(msg) {
state.playerId = msg.playerId;
try {
if (msg.playerId) localStorage.setItem('playerId', msg.playerId);
if (msg.playerId) localStorage.setItem("playerId", msg.playerId);
} catch {}
if (msg.sessionId) {
const existing = localStorage.getItem('sessionId');
const existing = localStorage.getItem("sessionId");
if (!existing) cacheSessionId(msg.sessionId);
}
// lazy import to avoid cycle
import('./session.js').then(({ reusePlayerName, reconnectLastRoom }) => {
import("./session.js").then(({ reusePlayerName, reconnectLastRoom }) => {
reusePlayerName();
reconnectLastRoom();
});
@@ -67,44 +66,41 @@ export function handlePlayTrack(msg) {
const t = msg.track;
state.lastTrack = t;
state.revealed = false;
$npTitle.textContent = '???';
$npArtist.textContent = '';
$npYear.textContent = '';
if ($guessTitle) $guessTitle.value = '';
if ($guessArtist) $guessArtist.value = '';
$npTitle.textContent = "???";
$npArtist.textContent = "";
$npYear.textContent = "";
if ($guessTitle) $guessTitle.value = "";
if ($guessArtist) $guessArtist.value = "";
if ($answerResult) {
$answerResult.textContent = '';
$answerResult.className = 'mt-1 text-sm';
$answerResult.textContent = "";
$answerResult.className = "mt-1 text-sm";
}
try {
$audio.preload = 'auto';
} catch {}
// Reset audio state before setting new source
try {
$audio.pause();
$audio.currentTime = 0;
} catch {}
// Load track with Howler, passing the filename for format detection
const sound = loadTrack(t.url, t.file);
$audio.src = t.url;
const pf = document.getElementById('progressFill');
if (pf) pf.style.width = '0%';
const rd = document.getElementById('recordDisc');
const pf = document.getElementById("progressFill");
if (pf) pf.style.width = "0%";
const rd = document.getElementById("recordDisc");
if (rd) {
rd.classList.remove('spin-record');
rd.src = '/hitstar.png';
rd.classList.remove("spin-record");
rd.src = "/hitstar.png";
}
const { startAt, serverNow } = msg;
const now = Date.now();
const offsetMs = startAt - serverNow;
const localStart = now + offsetMs;
const delay = Math.max(0, localStart - now);
setTimeout(() => {
// Don't reset currentTime again - it's already 0 from above
$audio.play().catch(() => {});
const disc = document.getElementById('recordDisc');
if (disc) disc.classList.add('spin-record');
if (sound && sound === getSound() && !sound.playing()) {
sound.play();
const disc = document.getElementById("recordDisc");
if (disc) disc.classList.add("spin-record");
}
}, delay);
if (state.room) renderRoom(state.room);
}
@@ -114,43 +110,47 @@ export function handleSync(msg) {
export function handleControl(msg) {
const { action, startAt, serverNow } = msg;
if (action === 'pause') {
$audio.pause();
const disc = document.getElementById('recordDisc');
if (disc) disc.classList.remove('spin-record');
$audio.playbackRate = 1.0;
} else if (action === 'play') {
const sound = getSound();
if (!sound) return;
if (action === "pause") {
sound.pause();
const disc = document.getElementById("recordDisc");
if (disc) disc.classList.remove("spin-record");
sound.rate(1.0);
} else if (action === "play") {
if (startAt && serverNow) {
const now = Date.now();
const elapsed = (now - startAt) / 1000;
$audio.currentTime = Math.max(0, elapsed);
sound.seek(Math.max(0, elapsed));
}
$audio.play().catch(() => {});
const disc = document.getElementById('recordDisc');
if (disc) disc.classList.add('spin-record');
sound.play();
const disc = document.getElementById("recordDisc");
if (disc) disc.classList.add("spin-record");
}
}
export function handleReveal(msg) {
const { result, track } = msg;
$npTitle.textContent = track.title || track.id || 'Track';
$npArtist.textContent = track.artist ? ` ${track.artist}` : '';
$npYear.textContent = track.year ? ` (${track.year})` : '';
$npTitle.textContent = track.title || track.id || "Track";
$npArtist.textContent = track.artist ? ` ${track.artist}` : "";
$npYear.textContent = track.year ? ` (${track.year})` : "";
state.revealed = true;
const $rb = document.getElementById('revealBanner');
const $rb = document.getElementById("revealBanner");
if ($rb) {
if (result.correct) {
$rb.textContent = 'Richtig!';
$rb.textContent = "Richtig!";
$rb.className =
'inline-block rounded-md bg-emerald-600 text-white px-3 py-1 text-sm font-medium';
"inline-block rounded-md bg-emerald-600 text-white px-3 py-1 text-sm font-medium";
} else {
$rb.textContent = 'Falsch!';
$rb.textContent = "Falsch!";
$rb.className =
'inline-block rounded-md bg-rose-600 text-white px-3 py-1 text-sm font-medium';
"inline-block rounded-md bg-rose-600 text-white px-3 py-1 text-sm font-medium";
}
}
// Note: placeArea visibility is now controlled by renderRoom() based on game phase
const rd = document.getElementById('recordDisc');
const rd = document.getElementById("recordDisc");
if (rd && track?.file) {
// Use track.file instead of track.id to include playlist folder prefix
const coverUrl = `/cover/${encodeURIComponent(track.file)}`;
@@ -173,58 +173,60 @@ export function handleGameEnded(msg) {
export function onMessage(ev) {
const msg = JSON.parse(ev.data);
switch (msg.type) {
case 'resume_result': {
case "resume_result": {
if (msg.ok) {
if (msg.playerId) {
state.playerId = msg.playerId;
try {
localStorage.setItem('playerId', msg.playerId);
localStorage.setItem("playerId", msg.playerId);
} catch {}
}
const code = msg.roomId || state.room?.id || localStorage.getItem('lastRoomId');
if (code) sendMsg({ type: 'join_room', roomId: code });
const code =
msg.roomId || state.room?.id || localStorage.getItem("lastRoomId");
if (code) sendMsg({ type: "join_room", roomId: code });
if (state.room) {
try {
renderRoom(state.room);
} catch {}
}
} else {
const code = state.room?.id || localStorage.getItem('lastRoomId');
if (code) sendMsg({ type: 'join_room', roomId: code });
const code = state.room?.id || localStorage.getItem("lastRoomId");
if (code) sendMsg({ type: "join_room", roomId: code });
}
return;
}
case 'connected':
case "connected":
return handleConnected(msg);
case 'room_update':
case "room_update":
return handleRoomUpdate(msg);
case 'play_track':
case "play_track":
return handlePlayTrack(msg);
case 'sync':
case "sync":
return handleSync(msg);
case 'control':
case "control":
return handleControl(msg);
case 'reveal':
case "reveal":
return handleReveal(msg);
case 'game_ended':
case "game_ended":
return handleGameEnded(msg);
case 'answer_result': {
case "answer_result": {
if ($answerResult) {
if (!msg.ok) {
$answerResult.textContent = '⛔ Eingabe ungültig oder gerade nicht möglich';
$answerResult.className = 'mt-1 text-sm text-rose-600';
$answerResult.textContent =
"⛔ Eingabe ungültig oder gerade nicht möglich";
$answerResult.className = "mt-1 text-sm text-rose-600";
} else {
const okBoth = !!(msg.correctTitle && msg.correctArtist);
const parts = [];
parts.push(msg.correctTitle ? 'Titel ✓' : 'Titel ✗');
parts.push(msg.correctArtist ? 'Künstler ✓' : 'Künstler ✗');
let coin = '';
if (msg.awarded) coin = ' +1 Token';
else if (msg.alreadyAwarded) coin = ' (bereits erhalten)';
$answerResult.textContent = `${parts.join(' · ')}${coin}`;
parts.push(msg.correctTitle ? "Titel ✓" : "Titel ✗");
parts.push(msg.correctArtist ? "Künstler ✓" : "Künstler ✗");
let coin = "";
if (msg.awarded) coin = " +1 Token";
else if (msg.alreadyAwarded) coin = " (bereits erhalten)";
$answerResult.textContent = `${parts.join(" · ")}${coin}`;
$answerResult.className = okBoth
? 'mt-1 text-sm text-emerald-600'
: 'mt-1 text-sm text-amber-600';
? "mt-1 text-sm text-emerald-600"
: "mt-1 text-sm text-amber-600";
}
}
return;