Spaces:
Running
Running
| <html lang="de"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>DE Radio Stream - Player (Updated)</title> | |
| <meta name="description" content="Der beste deutsche Online Radio Player mit funktionierenden Senderlisten."> | |
| <!-- Google Fonts Import --> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --bg-color: #0f172a; | |
| --surface-color: rgba(30, 41, 59, 0.7); | |
| --surface-border: rgba(255, 255, 255, 0.1); | |
| --primary-color: #6366f1; | |
| --primary-hover: #4f46e5; | |
| --accent-color: #ec4899; | |
| --text-main: #f8fafc; | |
| --text-muted: #94a3b8; | |
| --glass-blur: blur(12px); | |
| --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5); | |
| --radius-lg: 16px; | |
| --radius-md: 8px; | |
| --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| outline: none; | |
| } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background-color: var(--bg-color); | |
| background-image: | |
| radial-gradient(at 0% 0%, hsla(253, 16%, 7%, 1) 0, transparent 50%), | |
| radial-gradient(at 50% 0%, hsla(225, 39%, 30%, 1) 0, transparent 50%), | |
| radial-gradient(at 100% 0%, hsla(339, 49%, 30%, 1) 0, transparent 50%); | |
| color: var(--text-main); | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| overflow-x: hidden; | |
| } | |
| /* Custom Scrollbar */ | |
| ::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: rgba(0, 0, 0, 0.2); | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: var(--surface-border); | |
| border-radius: 4px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: var(--primary-color); | |
| } | |
| /* --- Header --- */ | |
| header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 1.5rem 2rem; | |
| background: rgba(15, 23, 42, 0.8); | |
| backdrop-filter: var(--glass-blur); | |
| border-bottom: 1px solid var(--surface-border); | |
| position: sticky; | |
| top: 0; | |
| z-index: 100; | |
| } | |
| .logo { | |
| font-size: 1.5rem; | |
| font-weight: 800; | |
| background: linear-gradient(to right, var(--primary-color), var(--accent-color)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| letter-spacing: -0.5px; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .anycoder-link { | |
| font-size: 0.85rem; | |
| color: var(--text-muted); | |
| text-decoration: none; | |
| padding: 0.5rem 1rem; | |
| border: 1px solid var(--surface-border); | |
| border-radius: 20px; | |
| transition: var(--transition); | |
| } | |
| .anycoder-link:hover { | |
| color: var(--text-main); | |
| border-color: var(--primary-color); | |
| background: rgba(99, 102, 241, 0.1); | |
| } | |
| /* --- Main Layout --- */ | |
| main { | |
| flex: 1; | |
| display: grid; | |
| grid-template-columns: 1fr 380px; | |
| gap: 2rem; | |
| padding: 2rem; | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| width: 100%; | |
| } | |
| @media (max-width: 900px) { | |
| main { | |
| grid-template-columns: 1fr; | |
| padding: 1rem; | |
| } | |
| } | |
| /* --- Station List (Left Side) --- */ | |
| .station-list-container { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1.5rem; | |
| } | |
| .search-bar { | |
| position: relative; | |
| } | |
| .search-bar input { | |
| width: 100%; | |
| padding: 1rem 1rem 1rem 3rem; | |
| background: var(--surface-color); | |
| border: 1px solid var(--surface-border); | |
| border-radius: var(--radius-lg); | |
| color: var(--text-main); | |
| font-size: 1rem; | |
| transition: var(--transition); | |
| } | |
| .search-bar input:focus { | |
| border-color: var(--primary-color); | |
| box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2); | |
| } | |
| .search-icon { | |
| position: absolute; | |
| left: 1rem; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| color: var(--text-muted); | |
| width: 20px; | |
| height: 20px; | |
| } | |
| .stations-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); | |
| gap: 1rem; | |
| } | |
| .station-card { | |
| background: var(--surface-color); | |
| border: 1px solid var(--surface-border); | |
| border-radius: var(--radius-md); | |
| padding: 1rem; | |
| cursor: pointer; | |
| transition: var(--transition); | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .station-card:hover { | |
| transform: translateY(-2px); | |
| background: rgba(255, 255, 255, 0.05); | |
| border-color: rgba(255, 255, 255, 0.2); | |
| } | |
| .station-card.active { | |
| background: rgba(99, 102, 241, 0.15); | |
| border-color: var(--primary-color); | |
| } | |
| .station-card.active::before { | |
| content: ''; | |
| position: absolute; | |
| left: 0; | |
| top: 0; | |
| bottom: 0; | |
| width: 4px; | |
| background: var(--primary-color); | |
| } | |
| .station-logo { | |
| width: 48px; | |
| height: 48px; | |
| border-radius: 50%; | |
| background: #333; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-weight: bold; | |
| font-size: 0.8rem; | |
| color: #fff; | |
| flex-shrink: 0; | |
| object-fit: cover; | |
| } | |
| .station-info h3 { | |
| font-size: 1rem; | |
| font-weight: 600; | |
| margin-bottom: 0.25rem; | |
| } | |
| .station-info p { | |
| font-size: 0.85rem; | |
| color: var(--text-muted); | |
| } | |
| .play-indicator { | |
| margin-left: auto; | |
| opacity: 0; | |
| color: var(--primary-color); | |
| transition: var(--transition); | |
| } | |
| .station-card:hover .play-indicator, | |
| .station-card.active .play-indicator { | |
| opacity: 1; | |
| } | |
| /* --- Player (Right Side / Sticky) --- */ | |
| .player-container { | |
| background: var(--surface-color); | |
| backdrop-filter: var(--glass-blur); | |
| border: 1px solid var(--surface-border); | |
| border-radius: var(--radius-lg); | |
| padding: 2rem; | |
| height: fit-content; | |
| position: sticky; | |
| top: 100px; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| box-shadow: var(--shadow-lg); | |
| text-align: center; | |
| } | |
| @media (max-width: 900px) { | |
| .player-container { | |
| position: relative; | |
| top: 0; | |
| order: -1; | |
| /* Player on top on mobile */ | |
| } | |
| } | |
| .player-visualizer { | |
| display: flex; | |
| align-items: flex-end; | |
| justify-content: center; | |
| gap: 4px; | |
| height: 60px; | |
| margin-bottom: 1.5rem; | |
| width: 100%; | |
| } | |
| .bar { | |
| width: 6px; | |
| background: var(--primary-color); | |
| border-radius: 4px; | |
| height: 10%; | |
| transition: height 0.1s ease; | |
| } | |
| .bar:nth-child(even) { | |
| background: var(--accent-color); | |
| } | |
| .player-visualizer.playing .bar { | |
| animation: bounce 1s infinite ease-in-out; | |
| } | |
| @keyframes bounce { | |
| 0%, | |
| 100% { | |
| height: 10%; | |
| } | |
| 50% { | |
| height: 100%; | |
| } | |
| } | |
| /* Stagger animations */ | |
| .bar:nth-child(1) { | |
| animation-duration: 0.8s; | |
| } | |
| .bar:nth-child(2) { | |
| animation-duration: 1.1s; | |
| } | |
| .bar:nth-child(3) { | |
| animation-duration: 0.9s; | |
| } | |
| .bar:nth-child(4) { | |
| animation-duration: 1.2s; | |
| } | |
| .bar:nth-child(5) { | |
| animation-duration: 0.7s; | |
| } | |
| .bar:nth-child(6) { | |
| animation-duration: 1.0s; | |
| } | |
| .bar:nth-child(7) { | |
| animation-duration: 0.8s; | |
| } | |
| .bar:nth-child(8) { | |
| animation-duration: 1.3s; | |
| } | |
| .current-station-art { | |
| width: 150px; | |
| height: 150px; | |
| border-radius: 20px; | |
| background: linear-gradient(135deg, #333, #111); | |
| margin-bottom: 1.5rem; | |
| box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 3rem; | |
| color: var(--text-muted); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .current-station-art img { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| } | |
| .current-station-name { | |
| font-size: 1.5rem; | |
| font-weight: 700; | |
| margin-bottom: 0.5rem; | |
| line-height: 1.2; | |
| } | |
| .current-station-genre { | |
| display: inline-block; | |
| background: rgba(255, 255, 255, 0.1); | |
| padding: 4px 12px; | |
| border-radius: 12px; | |
| font-size: 0.8rem; | |
| color: var(--text-muted); | |
| margin-bottom: 2rem; | |
| } | |
| .controls { | |
| display: flex; | |
| align-items: center; | |
| gap: 1.5rem; | |
| width: 100%; | |
| justify-content: center; | |
| margin-bottom: 2rem; | |
| } | |
| .btn-control { | |
| background: none; | |
| border: none; | |
| cursor: pointer; | |
| color: var(--text-main); | |
| transition: var(--transition); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .btn-control:hover { | |
| color: var(--primary-color); | |
| transform: scale(1.1); | |
| } | |
| .btn-play { | |
| width: 64px; | |
| height: 64px; | |
| border-radius: 50%; | |
| background: var(--primary-color); | |
| box-shadow: 0 0 20px rgba(99, 102, 241, 0.4); | |
| } | |
| .btn-play:hover { | |
| background: var(--primary-hover); | |
| color: white; | |
| box-shadow: 0 0 30px rgba(99, 102, 241, 0.6); | |
| } | |
| .volume-container { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| width: 100%; | |
| max-width: 200px; | |
| } | |
| input[type="range"] { | |
| -webkit-appearance: none; | |
| width: 100%; | |
| height: 6px; | |
| background: rgba(255, 255, 255, 0.1); | |
| border-radius: 3px; | |
| cursor: pointer; | |
| } | |
| input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 16px; | |
| height: 16px; | |
| background: var(--text-main); | |
| border-radius: 50%; | |
| transition: var(--transition); | |
| } | |
| input[type="range"]::-webkit-slider-thumb:hover { | |
| background: var(--primary-color); | |
| transform: scale(1.2); | |
| } | |
| /* --- Footer --- */ | |
| footer { | |
| text-align: center; | |
| padding: 2rem; | |
| color: var(--text-muted); | |
| font-size: 0.9rem; | |
| border-top: 1px solid var(--surface-border); | |
| margin-top: auto; | |
| } | |
| /* --- Loading State --- */ | |
| .loading-spinner { | |
| width: 20px; | |
| height: 20px; | |
| border: 2px solid rgba(255, 255, 255, 0.3); | |
| border-top-color: var(--primary-color); | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| display: none; | |
| } | |
| @keyframes spin { | |
| to { | |
| transform: rotate(360deg); | |
| } | |
| } | |
| .status-message { | |
| margin-top: 1rem; | |
| font-size: 0.85rem; | |
| min-height: 1.2em; | |
| color: var(--accent-color); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="logo"> | |
| <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" | |
| stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M12 2c-5.5 0-10 4.5-10 10s4.5 10 10 10c1.8 0 3.5-.5 5-1.3M15 5a5 5 0 0 1 5 5v1M15 9a3 3 0 0 1 3 3v1" /> | |
| <circle cx="12" cy="12" r="2" /> | |
| </svg> | |
| DE Radio | |
| </div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">Built with | |
| anycoder</a> | |
| </header> | |
| <main> | |
| <!-- Senderliste --> | |
| <section class="station-list-container"> | |
| <div class="search-bar"> | |
| <svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" | |
| stroke-linecap="round" stroke-linejoin="round"> | |
| <circle cx="11" cy="11" r="8"></circle> | |
| <line x1="21" y1="21" x2="16.65" y2="16.65"></line> | |
| </svg> | |
| <input type="text" id="searchInput" placeholder="Sender suchen..."> | |
| </div> | |
| <div class="stations-grid" id="stationsGrid"> | |
| <!-- Stationen werden hier per JS eingefügt --> | |
| </div> | |
| </section> | |
| <!-- Player --> | |
| <section class="player-container"> | |
| <div class="player-visualizer" id="visualizer"> | |
| <div class="bar"></div> | |
| <div class="bar"></div> | |
| <div class="bar"></div> | |
| <div class="bar"></div> | |
| <div class="bar"></div> | |
| <div class="bar"></div> | |
| <div class="bar"></div> | |
| <div class="bar"></div> | |
| </div> | |
| <div class="current-station-art" id="playerArt"> | |
| <svg width="50" height="50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" | |
| stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M9 18V5l12-2v13"></path> | |
| <circle cx="6" cy="18" r="3"></circle> | |
| <circle cx="18" cy="16" r="3"></circle> | |
| </svg> | |
| </div> | |
| <h2 class="current-station-name" id="playerTitle">Wähle einen Sender</h2> | |
| <span class="current-station-genre" id="playerGenre">Bereit</span> | |
| <div class="controls"> | |
| <button class="btn-control btn-play" id="playPauseBtn" aria-label="Play/Pause"> | |
| <svg id="iconPlay" width="32" height="32" viewBox="0 0 24 24" fill="currentColor"> | |
| <polygon points="5 3 19 12 5 21 5 3"></polygon> | |
| </svg> | |
| <svg id="iconPause" width="32" height="32" viewBox="0 0 24 24" fill="currentColor" style="display: none;"> | |
| <rect x="6" y="4" width="4" height="16"></rect> | |
| <rect x="14" y="4" width="4" height="16"></rect> | |
| </svg> | |
| <div class="loading-spinner" id="loadingSpinner"></div> | |
| </button> | |
| </div> | |
| <div class="volume-container"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" | |
| stroke-linecap="round" stroke-linejoin="round"> | |
| <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon> | |
| </svg> | |
| <input type="range" id="volumeSlider" min="0" max="1" step="0.01" value="0.8"> | |
| </div> | |
| <div class="status-message" id="statusMessage"></div> | |
| </section> | |
| </main> | |
| <footer> | |
| <p>© 2023 DE Radio Stream Player. Hören Sie Ihre Lieblingssender.</p> | |
| </footer> | |
| <script> | |
| // Aktualisierte Senderdaten mit HTTPS-Links (CORS-freundliche URLs wo möglich) | |
| const stations = [ | |
| { name: "Antenne Bayern", genre: "Pop", url: "https://stream.antenne.de/antenne-bayern", color: "#ea580c" }, | |
| { name: "BAYERN 3", genre: "Pop & Rock", url: "https://br-mp3-f-shz-cybr.sharp-stream.com/br3/live/mp3/128/stream.mp3", color: "#2563eb" }, | |
| { name: "1LIVE", genre: "Pop & Electro", url: "https://wdr-1live-live.icecast.wdr.de/wdr/1live/live/mp3/128/stream.mp3", color: "#7c3aed" }, | |
| { name: "WDR 2", genre: "Pop & Info", url: "https://wdr-wdr2-rheinland.icecast.wdr.de/wdr/wdr2/rheinland/mp3/128/stream.mp3", color: "#0891b2" }, | |
| { name: "SWR3", genre: "Pop", url: "https://swr-swr3-live.cast.addradio.de/swr/swr3/live/mp3/128/stream.mp3", color: "#db2777" }, | |
| { name: "NDR 2", genre: "Pop", url: "https://ndr-ndr2-niedersachsen.cast.addradio.de/ndr/ndr2/niedersachsen/mp3/128/stream.mp3", color: "#4338ca" }, | |
| { name: "NDR Info", genre: "Nachrichten", url: "https://ndr-ndrinfo-niedersachsen.cast.addradio.de/ndr/ndrinfo/niedersachsen/mp3/128/stream.mp3", color: "#334155" }, | |
| { name: "N-JOY", genre: "Pop", url: "https://ndr-njoy-live.cast.addradio.de/ndr/njoy/live/mp3/128/stream.mp3", color: "#16a34a" }, | |
| { name: "RBB 88.8", genre: "Berlin", url: "https://rbb-berlin-live.cast.addradio.de/rbb/berlin/live/mp3/128/stream.mp3", color: "#dc2626" }, | |
| { name: "RBB Radio Eins", genre: "Popkultur", url: "https://rbb-radioeins-live.cast.addradio.de/rbb/radioeins/live/mp3/128/stream.mp3", color: "#be185d" }, | |
| { name: "HR 3", genre: "Pop", url: "https://hr-hr3-live.cast.addradio.de/hr/hr3/live/mp3/128/stream.mp3", color: "#ca8a04" }, | |
| { name: "MDR JUMP", genre: "Dance & Pop", url: "https://mdr-jump-live.cast.addradio.de/mdr/jump/live/mp3/128/stream.mp3", color: "#65a30d" }, | |
| { name: "Deutschlandfunk", genre: "Nachrichten & Info", url: "https://st01.dlf.de/dlf/01/128/mp3/stream.mp3", color: "#e11d48" }, | |
| { name: "DRadio Wissen", genre: "Wissen", url: "https://st03.dlf.de/dlf/03/128/mp3/stream.mp3", color: "#059669" }, | |
| { name: "KISS FM", genre: "Dance & R&B", url: "https://stream.kissfm.de/kissfm/mp3-128", color: "#c026d3" }, | |
| { name: "JAM FM", genre: "Black & Pop", url: "https://stream.jam.fm/jamfm-live/mp3-128", color: "#f59e0b" }, | |
| { name: "Radio BOB", genre: "Rock", url: "https://stream.radiobob.de/bob-national/mp3-192", color: "#7f1d1d" }, | |
| { name: "105'5 Spreeradio", genre: "Berlin Hits", url: "https://stream.spreeradio.de/spreeradio-live-mp3-128", color: "#0ea5e9" }, | |
| { name: "Radio Paloma", genre: "Schlager", url: "https://stream.radio-paloma.de/live/mp3-192", color: "#f472b6" }, | |
| { name: "FFN", genre: "Pop & Rock", url: "https://stream.ffn.de/ffn/mp3-192", color: "#14b8a6" } | |
| ]; | |
| // DOM Elemente | |
| const stationsGrid = document.getElementById('stationsGrid'); | |
| const searchInput = document.getElementById('searchInput'); | |
| const audio = new Audio(); | |
| audio.crossOrigin = "anonymous"; // Wichtig für CORS | |
| const playerTitle = document.getElementById('playerTitle'); | |
| const playerGenre = document.getElementById('playerGenre'); | |
| const playerArt = document.getElementById('playerArt'); | |
| const playPauseBtn = document.getElementById('playPauseBtn'); | |
| const iconPlay = document.getElementById('iconPlay'); | |
| const iconPause = document.getElementById('iconPause'); | |
| const loadingSpinner = document.getElementById('loadingSpinner'); | |
| const volumeSlider = document.getElementById('volumeSlider'); | |
| const visualizer = document.getElementById('visualizer'); | |
| const statusMessage = document.getElementById('statusMessage'); | |
| let currentStationIndex = -1; | |
| let isPlaying = false; | |
| // Initialisierung | |
| function init() { | |
| renderStations(stations); | |
| audio.volume = volumeSlider.value; | |
| } | |
| // Stationen rendern | |
| function renderStations(list) { | |
| stationsGrid.innerHTML = ''; | |
| list.forEach((station, index) => { | |
| const card = document.createElement('div'); | |
| card.className = 'station-card'; | |
| // Wir speichern den Namen im Dataset, um das richtige Element wiederzufinden | |
| card.dataset.name = station.name; | |
| card.onclick = () => selectStation(station); | |
| // Initialen für das Logo | |
| const initials = station.name.substring(0, 2).toUpperCase(); | |
| card.innerHTML = ` | |
| <div class="station-logo" style="background-color: ${station.color}"> | |
| ${initials} | |
| </div> | |
| <div class="station-info"> | |
| <h3>${station.name}</h3> | |
| <p>${station.genre}</p> | |
| </div> | |
| <div class="play-indicator"> | |
| <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"> | |
| <polygon points="5 3 19 12 5 21 5 3"></polygon> | |
| </svg> | |
| </div> | |
| `; | |
| stationsGrid.appendChild(card); | |
| }); | |
| } | |
| // Suche | |
| searchInput.addEventListener('input', (e) => { | |
| const term = e.target.value.toLowerCase(); | |
| const filtered = stations.filter(s => | |
| s.name.toLowerCase().includes(term) || | |
| s.genre.toLowerCase().includes(term) | |
| ); | |
| renderStations(filtered); | |
| }); | |
| // Station auswählen | |
| function selectStation(station) { | |
| // Wenn gleiche Station geklickt wird, nur Play/Pause toggeln | |
| if (playerTitle.innerText === station.name) { | |
| togglePlay(); | |
| return; | |
| } | |
| // UI Update | |
| playerTitle.innerText = station.name; | |
| playerGenre.innerText = station.genre; | |
| playerArt.style.backgroundColor = station.color; | |
| playerArt.innerHTML = `<span style="font-size: 3rem; font-weight: 800; color: rgba(255,255,255,0.8);">${station.name.substring(0,1).toUpperCase()}</span>`; | |
| // Highlight in Liste | |
| document.querySelectorAll('.station-card').forEach(card => { | |
| card.classList.remove('active'); | |
| if (card.dataset.name === station.name) { | |
| card.classList.add('active'); | |
| } | |
| }); | |
| // Audio laden | |
| statusMessage.innerText = "Verbinde..."; | |
| statusMessage.style.color = "var(--text-muted)"; | |
| // Reset Audio Source | |
| audio.src = station.url; | |
| audio.load(); | |
| playAudio(); | |
| } | |
| // Play/Pause Logik | |
| function togglePlay() { | |
| if (!audio.src) return; | |
| if (audio.paused) { | |
| playAudio(); | |
| } else { | |
| pauseAudio(); | |
| } | |
| } | |
| function playAudio() { | |
| const playPromise = audio.play(); | |
| if (playPromise !== undefined) { | |
| playPromise.then(_ => { | |
| isPlaying = true; | |
| updatePlayButton(); | |
| visualizer.classList.add('playing'); | |
| statusMessage.innerText = ""; | |
| }) | |
| .catch(error => { | |
| console.error("Playback error:", error); | |
| isPlaying = false; | |
| updatePlayButton(); | |
| visualizer.classList.remove('playing'); | |
| statusMessage.innerText = "Fehler: Stream blockiert oder offline."; | |
| statusMessage.style.color = "#ef4444"; | |
| }); | |
| } | |
| } | |
| function pauseAudio() { | |
| audio.pause(); | |
| isPlaying = false; | |
| updatePlayButton(); | |
| visualizer.classList.remove('playing'); | |
| } | |
| function updatePlayButton() { | |
| if (isPlaying) { | |
| iconPlay.style.display = 'none'; | |
| iconPause.style.display = 'block'; | |
| loadingSpinner.style.display = 'none'; | |
| } else { | |
| iconPlay.style.display = 'block'; | |
| iconPause.style.display = 'none'; | |
| loadingSpinner.style.display = 'none'; | |
| } | |
| } | |
| playPauseBtn.addEventListener('click', togglePlay); | |
| // Lautstärke | |
| volumeSlider.addEventListener('input', (e) => { | |
| audio.volume = e.target.value; | |
| }); | |
| // Audio Events | |
| audio.addEventListener('waiting', () => { | |
| loadingSpinner.style.display = 'block'; | |
| iconPlay.style.display = 'none'; | |
| iconPause.style.display = 'none'; | |
| statusMessage.innerText = "Puffer..."; | |
| }); | |
| audio.addEventListener('playing', () => { | |
| loadingSpinner.style.display = 'none'; | |
| updatePlayButton(); | |
| }); | |
| audio.addEventListener('error', (e) => { | |
| console.error("Stream Error", e); | |
| loadingSpinner.style.display = 'none'; | |
| iconPlay.style.display = 'block'; | |
| iconPause.style.display = 'none'; | |
| visualizer.classList.remove('playing'); | |
| statusMessage.innerText = "Stream nicht verfügbar (CORS/Offline)."; | |
| statusMessage.style.color = "#ef4444"; | |
| }); | |
| // App starten | |
| init(); | |
| </script> | |
| </body> | |
| </html> |