ffmpeg implemented

This commit is contained in:
2026-01-11 13:40:01 +01:00
parent 34c3f0dc89
commit c4020615d2
18 changed files with 474 additions and 16 deletions

View File

@@ -20,6 +20,7 @@ const nowArtistEl = document.getElementById('now-artist');
const nowTitleEl = document.getElementById('now-title');
const statusTextEl = document.getElementById('status-text');
const statusDotEl = document.querySelector('.status-dot');
const engineBadgeEl = document.getElementById('engine-badge');
const playBtn = document.getElementById('play-btn');
const iconPlay = document.getElementById('icon-play');
const iconStop = document.getElementById('icon-stop');
@@ -35,6 +36,8 @@ const coverflowStageEl = document.getElementById('artwork-coverflow-stage');
const coverflowPrevBtn = document.getElementById('artwork-prev');
const coverflowNextBtn = document.getElementById('artwork-next');
const artworkPlaceholder = document.querySelector('.artwork-placeholder');
const logoTextEl = document.querySelector('.station-logo-text');
const logoImgEl = document.getElementById('station-logo-img');
// Global error handlers to avoid silent white screen and show errors in UI
window.addEventListener('error', (ev) => {
try {
@@ -69,12 +72,43 @@ async function init() {
setupEventListeners();
ensureArtworkPointerFallback();
updateUI();
updateEngineBadge();
} catch (e) {
console.error('Error during init', e);
if (statusTextEl) statusTextEl.textContent = 'Init error: ' + (e && e.message ? e.message : String(e));
}
}
function updateEngineBadge() {
if (!engineBadgeEl) return;
// In this app:
// - Local playback uses the native backend (FFmpeg decode + CPAL output).
// - Cast mode plays via Chromecast.
const kind = currentMode === 'cast' ? 'cast' : 'ffmpeg';
const label = kind === 'cast' ? 'CAST' : 'FFMPEG';
const title = kind === 'cast' ? 'Google Cast playback' : 'Native playback (FFmpeg)';
const iconSvg = kind === 'cast'
? `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M2 16.1A5 5 0 0 1 5.9 20" />
<path d="M2 12.05A9 9 0 0 1 9.95 20" />
<path d="M2 8V6a14 14 0 0 1 14 14h-2" />
</svg>`
: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M4 15V9" />
<path d="M8 19V5" />
<path d="M12 16V8" />
<path d="M16 18V6" />
<path d="M20 15V9" />
</svg>`;
engineBadgeEl.innerHTML = `${iconSvg}<span>${label}</span>`;
engineBadgeEl.title = title;
engineBadgeEl.classList.remove('engine-ffmpeg', 'engine-cast', 'engine-html');
engineBadgeEl.classList.add(`engine-${kind}`);
}
// Volume persistence
function saveVolumeToStorage(val) {
try {
@@ -134,6 +168,15 @@ function startLocalPlayerStatePolling() {
} else if (status === 'error') {
statusTextEl.textContent = st.error ? `Error: ${st.error}` : 'Error';
statusDotEl.style.backgroundColor = 'var(--danger)';
// Backend is no longer playing; reflect that in UX.
isPlaying = false;
stopLocalPlayerStatePolling();
updateUI();
} else if (status === 'stopped' || status === 'idle') {
isPlaying = false;
stopLocalPlayerStatePolling();
updateUI();
} else {
// idle/stopped: keep UI consistent with our isPlaying flag
}
@@ -884,6 +927,44 @@ function loadStation(index) {
if (nowArtistEl) nowArtistEl.textContent = '';
if (nowTitleEl) nowTitleEl.textContent = '';
// Update main artwork logo (best-effort). Many station logo URLs are http; try https first.
try {
if (logoTextEl && station && station.name) {
const numberMatch = station.name.match(/\d+/);
logoTextEl.textContent = numberMatch ? numberMatch[0] : station.name.charAt(0).toUpperCase();
}
const rawLogo = (station && (station.logo || (station.raw && (station.raw.logo || station.raw.poster)))) || '';
const logoUrl = rawLogo && rawLogo.startsWith('http://') ? ('https://' + rawLogo.slice('http://'.length)) : rawLogo;
if (logoImgEl) {
logoImgEl.onload = null;
logoImgEl.onerror = null;
if (logoUrl) {
logoImgEl.onload = () => {
logoImgEl.classList.remove('hidden');
if (logoTextEl) logoTextEl.classList.add('hidden');
};
logoImgEl.onerror = () => {
logoImgEl.classList.add('hidden');
if (logoTextEl) logoTextEl.classList.remove('hidden');
};
logoImgEl.src = logoUrl;
// Show fallback until load completes.
logoImgEl.classList.add('hidden');
if (logoTextEl) logoTextEl.classList.remove('hidden');
} else {
logoImgEl.src = '';
logoImgEl.classList.add('hidden');
if (logoTextEl) logoTextEl.classList.remove('hidden');
}
}
} catch (e) {
// non-fatal
}
// Sync coverflow transforms (if present)
try { updateCoverflowTransforms(); } catch (e) {}
// When loading a station, ensure only this station's poller runs
@@ -1021,6 +1102,8 @@ function updateUI() {
statusDotEl.style.backgroundColor = 'var(--text-muted)';
stationSubtitleEl.textContent = currentMode === 'cast' ? `Connected to ${currentCastDevice}` : 'Live Stream';
}
updateEngineBadge();
}
function handleVolumeInput() {
@@ -1105,13 +1188,29 @@ async function selectCastDevice(deviceName) {
window.addEventListener('DOMContentLoaded', init);
// Register Service Worker for PWA installation (non-disruptive)
// Service worker is useful for the PWA, but it can cause confusing caching during
// Tauri development because it may serve an older cached `index.html`.
const runningInTauri = !!(window.__TAURI__ && window.__TAURI__.core);
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('sw.js')
.then((reg) => console.log('ServiceWorker registered:', reg.scope))
.catch((err) => console.debug('ServiceWorker registration failed:', err));
});
if (runningInTauri) {
// Best-effort cleanup so the desktop app always reflects local file changes.
navigator.serviceWorker.getRegistrations()
.then((regs) => Promise.all(regs.map((r) => r.unregister())))
.catch(() => {});
if ('caches' in window) {
caches.keys()
.then((keys) => Promise.all(keys.map((k) => caches.delete(k))))
.catch(() => {});
}
} else {
// Register Service Worker for PWA installation (non-disruptive)
window.addEventListener('load', () => {
navigator.serviceWorker.register('sw.js')
.then((reg) => console.log('ServiceWorker registered:', reg.scope))
.catch((err) => console.debug('ServiceWorker registration failed:', err));
});
}
}
// Open overlay and show list of stations (used by menu/hamburger)