ffmpeg implemented
This commit is contained in:
@@ -96,6 +96,9 @@
|
||||
<span class="blob b10"></span>
|
||||
</div>
|
||||
|
||||
<img id="station-logo-img" class="station-logo-img hidden" alt="station logo">
|
||||
<span class="station-logo-text">1</span>
|
||||
|
||||
<!-- Coverflow-style station carousel inside the artwork (drag or use arrows) -->
|
||||
<div id="artwork-coverflow" class="artwork-coverflow" aria-label="Stations">
|
||||
<button id="artwork-prev" class="coverflow-arrow left" aria-label="Previous station">‹</button>
|
||||
@@ -116,6 +119,7 @@
|
||||
<div id="status-indicator" class="status-indicator-wrap" aria-hidden="true">
|
||||
<span class="status-dot"></span>
|
||||
<span id="status-text"></span>
|
||||
<span id="engine-badge" class="engine-badge" title="Playback engine">FFMPEG</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
111
src/main.js
111
src/main.js
@@ -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)
|
||||
|
||||
@@ -211,6 +211,31 @@ body {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.engine-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.6px;
|
||||
text-transform: uppercase;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(255,255,255,0.12);
|
||||
background: rgba(255,255,255,0.06);
|
||||
color: var(--text-main);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.engine-badge svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.engine-ffmpeg { border-color: rgba(125,255,179,0.30); box-shadow: 0 0 10px rgba(125,255,179,0.12); }
|
||||
.engine-cast { border-color: rgba(223,166,255,0.35); box-shadow: 0 0 10px rgba(223,166,255,0.12); }
|
||||
.engine-html { border-color: rgba(255,255,255,0.22); }
|
||||
|
||||
.status-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
@@ -341,6 +366,7 @@ body {
|
||||
box-shadow: 0 8px 20px rgba(0,0,0,0.35);
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
margin-left:1rem;
|
||||
}
|
||||
|
||||
/* Logo blobs container sits behind logo but inside artwork placeholder */
|
||||
|
||||
13
src/sw.js
13
src/sw.js
@@ -1,4 +1,4 @@
|
||||
const CACHE_NAME = 'radiocast-core-v1';
|
||||
const CACHE_NAME = 'radiocast-core-v2';
|
||||
const CORE_ASSETS = [
|
||||
'.',
|
||||
'index.html',
|
||||
@@ -11,6 +11,8 @@ const CORE_ASSETS = [
|
||||
];
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
// Activate updated SW as soon as it's installed.
|
||||
self.skipWaiting();
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME).then((cache) => cache.addAll(CORE_ASSETS))
|
||||
);
|
||||
@@ -18,9 +20,12 @@ self.addEventListener('install', (event) => {
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then((keys) => Promise.all(
|
||||
keys.map((k) => { if (k !== CACHE_NAME) return caches.delete(k); return null; })
|
||||
))
|
||||
Promise.all([
|
||||
self.clients.claim(),
|
||||
caches.keys().then((keys) => Promise.all(
|
||||
keys.map((k) => { if (k !== CACHE_NAME) return caches.delete(k); return null; })
|
||||
)),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user