From f8bf23dd4f522db74bf666c0df70bf3f0e86ef87 Mon Sep 17 00:00:00 2001 From: codey Date: Sat, 19 Jul 2025 08:50:04 -0400 Subject: [PATCH] radio web player changes / iOS support (still WIP, only main station supported; iOS Safari does not support streaming of Ogg/Vorbis, so an HLS stream output was created for that station. --- src/assets/styles/player.css | 12 ++ src/components/AudioPlayer.jsx | 193 +++++++++++++-------------------- 2 files changed, 89 insertions(+), 116 deletions(-) diff --git a/src/assets/styles/player.css b/src/assets/styles/player.css index 7901c55..4c8db88 100644 --- a/src/assets/styles/player.css +++ b/src/assets/styles/player.css @@ -266,3 +266,15 @@ body { width: 24px !important; } } +.tap-overlay { + position: fixed; + inset: 0; + z-index: 50; + background-color: rgba(0, 0, 0, 0.8); + color: white; + font-size: 1.5rem; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; +} diff --git a/src/components/AudioPlayer.jsx b/src/components/AudioPlayer.jsx index d48a621..ba30e0c 100644 --- a/src/components/AudioPlayer.jsx +++ b/src/components/AudioPlayer.jsx @@ -6,7 +6,7 @@ import "@styles/player.css"; const API_URL = "https://api.codey.lol"; Howler.html5PoolSize = 32; - +const isIOS = /iP(ad|hone|od)/.test(navigator.userAgent); const STATIONS = { main: { label: "Main", streamPath: "/sfm.ogg" }, rock: { label: "Rock", streamPath: "/rock.ogg" }, @@ -16,12 +16,6 @@ const STATIONS = { pop: { label: "Pop", streamPath: "/pop.ogg" }, }; -// Detect iOS user agent (can refine if needed) -const isIOS = (() => { - if (typeof window === "undefined") return false; - return /iP(ad|hone|od)/.test(navigator.userAgent); -})(); - let activeInterval = null; let currentStationForInterval = null; @@ -42,6 +36,7 @@ export function Player() { const [coverArt, setCoverArt] = useState("/images/radio_art_default.jpg"); const [elapsed, setElapsed] = useState(0); const [duration, setDuration] = useState(0); + const [userHasInteracted, setUserHasInteracted] = useState(false); const soundRef = useRef(null); const uuidRef = useRef(null); @@ -55,131 +50,71 @@ export function Player() { const progress = duration > 0 ? (elapsed / duration) * 100 : 0; - // UseEffect: create Howl on station change (as before) - useEffect(() => { + const playStream = () => { + let streamPath = STATIONS[activeStation].streamPath; + if (activeStation === "main" && isIOS) { + streamPath = "/hls/sfm.m3u8"; + } + const streamUrl = + "https://stream.codey.lol" + + streamPath + + `?t=${Date.now()}`; + if (soundRef.current) { + soundRef.current.stop(); soundRef.current.unload(); - soundRef.current = null; } - // On iOS we defer creation until user plays, so skip creating Howl here - if (isIOS) { - setIsPlaying(false); - return; - } - - const streamUrl = "https://stream.codey.lol" + STATIONS[activeStation].streamPath; - - const howl = new Howl({ + const newHowl = new Howl({ src: [streamUrl], html5: true, - onend: () => howl.play(), + onend: () => newHowl.play(), onplay: () => setIsPlaying(true), onpause: () => setIsPlaying(false), onstop: () => setIsPlaying(false), - onloaderror: (id, err) => console.error("Load error", err), - onplayerror: (id, err) => { + onloaderror: (_, err) => console.error("Load error", err), + onplayerror: (_, err) => { console.error("Play error", err); - setTimeout(() => howl.play(), 1000); + setTimeout(() => newHowl.play(), 1000); }, }); - soundRef.current = howl; - uuidRef.current = null; - lastStationData.current = null; - - // Autoplay if previously playing - if (isPlaying) { - setTimeout(() => { - if (soundRef.current) { - soundRef.current.play(); - } - }, 100); - } - - return () => { - howl.stop(); - howl.unload(); - }; - }, [activeStation]); - - // Toggle playback handler - const togglePlayback = () => { - if (isPlaying) { - if (soundRef.current) { - soundRef.current.pause(); - } - setIsPlaying(false); - } else { - if (isIOS) { - // On iOS, defer creation until user explicitly plays - if (soundRef.current) { - soundRef.current.stop(); - soundRef.current.unload(); - soundRef.current = null; - } - - const streamUrl = - "https://stream.codey.lol" + - STATIONS[activeStation].streamPath + - `?t=${Date.now()}`; // Cache-busting param - - const newHowl = new Howl({ - src: [streamUrl], - html5: true, - onend: () => newHowl.play(), - onplay: () => { - setIsPlaying(true); - }, - onpause: () => setIsPlaying(false), - onstop: () => setIsPlaying(false), - onloaderror: (_, err) => console.error("Load error", err), - onplayerror: (_, err) => { - console.error("Play error", err); - setTimeout(() => newHowl.play(), 1000); - }, - }); - - soundRef.current = newHowl; - newHowl.play(); - } else { - // Desktop browsers: current logic - if (soundRef.current) { - soundRef.current.stop(); - soundRef.current.unload(); - } - - const streamUrl = - "https://stream.codey.lol" + - STATIONS[activeStation].streamPath + - `?t=${Date.now()}`; // Cache-busting param - - const newHowl = new Howl({ - src: [streamUrl], - html5: true, - onend: () => newHowl.play(), - onplay: () => { - setIsPlaying(true); - }, - onpause: () => setIsPlaying(false), - onstop: () => setIsPlaying(false), - onloaderror: (_, err) => console.error("Load error", err), - onplayerror: (_, err) => { - console.error("Play error", err); - setTimeout(() => newHowl.play(), 1000); - }, - }); - - soundRef.current = newHowl; - newHowl.play(); - } - } + soundRef.current = newHowl; + newHowl.play(); }; - // Metadata fetcher (unchanged) +const togglePlayback = () => { + if (isIOS && !userHasInteracted) return; // block only on iOS until interaction + if (isPlaying) { + soundRef.current?.pause(); + setIsPlaying(false); + } else { + if (!soundRef.current) { + playStream(); + } else { + soundRef.current.play(); + } + setIsPlaying(true); + } +}; + + + useEffect(() => { + if (!userHasInteracted) return; + + if (isPlaying) { + playStream(); + } else { + if (soundRef.current) { + soundRef.current.stop(); + soundRef.current.unload(); + soundRef.current = null; + } + } + }, [activeStation]); + useEffect(() => { clearGlobalMetadataInterval(); - currentStationForInterval = activeStation; const fetchTrackData = async () => { @@ -235,6 +170,25 @@ export function Player() { return ( <> + {/* Unlock overlay */} + {!userHasInteracted && isIOS && ( +
{ + setUserHasInteracted(true); + + if (Howler.ctx?.state === "suspended") { + Howler.ctx.resume(); + } + + playStream(); // <-- Immediate play within same gesture + }} + + > + Tap to Start Audio +
+ )} +
{Object.entries(STATIONS).map(([key, { label }]) => (
-
+
{!isPlaying ? : }