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:
		@@ -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 && (
 | 
			
		||||
        <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">
 | 
			
		||||
        {Object.entries(STATIONS).map(([key, { label }]) => (
 | 
			
		||||
          <button
 | 
			
		||||
@@ -270,7 +224,14 @@ export function Player() {
 | 
			
		||||
              <div id="length" style={{ width: `${progress}%` }}></div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <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 />}
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user