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.
This commit is contained in:
@@ -266,3 +266,15 @@ body {
|
|||||||
width: 24px !important;
|
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;
|
||||||
|
}
|
||||||
|
@@ -6,7 +6,7 @@ import "@styles/player.css";
|
|||||||
|
|
||||||
const API_URL = "https://api.codey.lol";
|
const API_URL = "https://api.codey.lol";
|
||||||
Howler.html5PoolSize = 32;
|
Howler.html5PoolSize = 32;
|
||||||
|
const isIOS = /iP(ad|hone|od)/.test(navigator.userAgent);
|
||||||
const STATIONS = {
|
const STATIONS = {
|
||||||
main: { label: "Main", streamPath: "/sfm.ogg" },
|
main: { label: "Main", streamPath: "/sfm.ogg" },
|
||||||
rock: { label: "Rock", streamPath: "/rock.ogg" },
|
rock: { label: "Rock", streamPath: "/rock.ogg" },
|
||||||
@@ -16,12 +16,6 @@ const STATIONS = {
|
|||||||
pop: { label: "Pop", streamPath: "/pop.ogg" },
|
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 activeInterval = null;
|
||||||
let currentStationForInterval = null;
|
let currentStationForInterval = null;
|
||||||
|
|
||||||
@@ -42,6 +36,7 @@ export function Player() {
|
|||||||
const [coverArt, setCoverArt] = useState("/images/radio_art_default.jpg");
|
const [coverArt, setCoverArt] = useState("/images/radio_art_default.jpg");
|
||||||
const [elapsed, setElapsed] = useState(0);
|
const [elapsed, setElapsed] = useState(0);
|
||||||
const [duration, setDuration] = useState(0);
|
const [duration, setDuration] = useState(0);
|
||||||
|
const [userHasInteracted, setUserHasInteracted] = useState(false);
|
||||||
|
|
||||||
const soundRef = useRef(null);
|
const soundRef = useRef(null);
|
||||||
const uuidRef = useRef(null);
|
const uuidRef = useRef(null);
|
||||||
@@ -55,131 +50,71 @@ export function Player() {
|
|||||||
|
|
||||||
const progress = duration > 0 ? (elapsed / duration) * 100 : 0;
|
const progress = duration > 0 ? (elapsed / duration) * 100 : 0;
|
||||||
|
|
||||||
// UseEffect: create Howl on station change (as before)
|
const playStream = () => {
|
||||||
useEffect(() => {
|
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) {
|
if (soundRef.current) {
|
||||||
|
soundRef.current.stop();
|
||||||
soundRef.current.unload();
|
soundRef.current.unload();
|
||||||
soundRef.current = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// On iOS we defer creation until user plays, so skip creating Howl here
|
const newHowl = new Howl({
|
||||||
if (isIOS) {
|
|
||||||
setIsPlaying(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const streamUrl = "https://stream.codey.lol" + STATIONS[activeStation].streamPath;
|
|
||||||
|
|
||||||
const howl = new Howl({
|
|
||||||
src: [streamUrl],
|
src: [streamUrl],
|
||||||
html5: true,
|
html5: true,
|
||||||
onend: () => howl.play(),
|
onend: () => newHowl.play(),
|
||||||
onplay: () => setIsPlaying(true),
|
onplay: () => setIsPlaying(true),
|
||||||
onpause: () => setIsPlaying(false),
|
onpause: () => setIsPlaying(false),
|
||||||
onstop: () => setIsPlaying(false),
|
onstop: () => setIsPlaying(false),
|
||||||
onloaderror: (id, err) => console.error("Load error", err),
|
onloaderror: (_, err) => console.error("Load error", err),
|
||||||
onplayerror: (id, err) => {
|
onplayerror: (_, err) => {
|
||||||
console.error("Play error", err);
|
console.error("Play error", err);
|
||||||
setTimeout(() => howl.play(), 1000);
|
setTimeout(() => newHowl.play(), 1000);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
soundRef.current = howl;
|
soundRef.current = newHowl;
|
||||||
uuidRef.current = null;
|
newHowl.play();
|
||||||
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 = () => {
|
||||||
const togglePlayback = () => {
|
if (isIOS && !userHasInteracted) return; // block only on iOS until interaction
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
if (soundRef.current) {
|
soundRef.current?.pause();
|
||||||
soundRef.current.pause();
|
|
||||||
}
|
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
} else {
|
} else {
|
||||||
if (isIOS) {
|
if (!soundRef.current) {
|
||||||
// On iOS, defer creation until user explicitly plays
|
playStream();
|
||||||
|
} else {
|
||||||
|
soundRef.current.play();
|
||||||
|
}
|
||||||
|
setIsPlaying(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!userHasInteracted) return;
|
||||||
|
|
||||||
|
if (isPlaying) {
|
||||||
|
playStream();
|
||||||
|
} else {
|
||||||
if (soundRef.current) {
|
if (soundRef.current) {
|
||||||
soundRef.current.stop();
|
soundRef.current.stop();
|
||||||
soundRef.current.unload();
|
soundRef.current.unload();
|
||||||
soundRef.current = null;
|
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();
|
|
||||||
}
|
}
|
||||||
|
}, [activeStation]);
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Metadata fetcher (unchanged)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
clearGlobalMetadataInterval();
|
clearGlobalMetadataInterval();
|
||||||
|
|
||||||
currentStationForInterval = activeStation;
|
currentStationForInterval = activeStation;
|
||||||
|
|
||||||
const fetchTrackData = async () => {
|
const fetchTrackData = async () => {
|
||||||
@@ -235,6 +170,25 @@ export function Player() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{/* Unlock overlay */}
|
||||||
|
{!userHasInteracted && isIOS && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 bg-black bg-opacity-80 flex items-center justify-center text-white text-xl cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
setUserHasInteracted(true);
|
||||||
|
|
||||||
|
if (Howler.ctx?.state === "suspended") {
|
||||||
|
Howler.ctx.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
playStream(); // <-- Immediate play within same gesture
|
||||||
|
}}
|
||||||
|
|
||||||
|
>
|
||||||
|
Tap to Start Audio
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="station-tabs flex gap-2 justify-center mb-4 flex-wrap z-10 relative">
|
<div className="station-tabs flex gap-2 justify-center mb-4 flex-wrap z-10 relative">
|
||||||
{Object.entries(STATIONS).map(([key, { label }]) => (
|
{Object.entries(STATIONS).map(([key, { label }]) => (
|
||||||
<button
|
<button
|
||||||
@@ -270,7 +224,14 @@ export function Player() {
|
|||||||
<div id="length" style={{ width: `${progress}%` }}></div>
|
<div id="length" style={{ width: `${progress}%` }}></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="music-control">
|
<div className="music-control">
|
||||||
<div className="music-control__play" id="play" onClick={togglePlayback} role="button" tabIndex={0} aria-pressed={isPlaying}>
|
<div
|
||||||
|
className="music-control__play"
|
||||||
|
id="play"
|
||||||
|
onClick={togglePlayback}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-pressed={isPlaying}
|
||||||
|
>
|
||||||
{!isPlaying ? <Play /> : <Pause />}
|
{!isPlaying ? <Play /> : <Pause />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user