lofi-app/frontend/src/hooks/useAudioPlayer.ts
opencode 55244b5027 fix: resolve 35 open issues across backend, Android, frontend, Docker
Backend (src/main.py):
- Add shared httpx.Client singleton for connection reuse (#24)
- Add cache eviction for expired entries (#23)
- Expand CORS to allow POST and OPTIONS (#25)
- Preserve HLS tags (EXT-X-TARGETDURATION, EXT-X-MAP, etc) in rewrite (#12)
- Replace urllib with httpx.stream for direct streaming

Backend (src/modules/stream_extractor.py):
- Use extract_flat='in_playlist' for reduced latency (#35)

Tests (tests/integration/test_api.py):
- Parameterize channel count assertion against CHANNELS (#20)

Android (Channel.kt):
- Change mutable vars to immutable vals, use copy() pattern (#19)

Android (ServerApi.kt):
- Fix JSON parsing for bare array response (#21)
- Close response body on error path (#29)

Android (YouTubeExtractor.kt):
- Add ConnectionPool config (10 idle, 30s keepalive) (#28)
- Tighten findHlsUrl to require audio codec (#26)

Android (AudioPlayer.kt):
- Remove false error on STATE_READY + !isPlaying (#33)

Android (MainActivity.kt):
- Reuse fragments via findFragmentByTag + show/hide (#18)
- Move AudioPlayer.release() to Activity.onDestroy (#17)

Android (LofiViewModel.kt):
- Wrap YouTubeExtractor calls in withContext(Dispatchers.IO) (#16)
- Use immutable channel copies in discoverAllChannels (#19)
- Remove audioPlayer.release() from onCleared (#17)

Android (ChannelListFragment.kt):
- Tie swipe refresh to isDiscovering LiveData (#32)

Android (AndroidManifest.xml):
- Set allowBackup=false (#15)

Android (Preferences.kt):
- Remove hardcoded IP, default to empty string (#14)

Android (proguard-rules.pro):
- Add ExoPlayer media3 HLS ProGuard rules (#34)

Frontend (useAudioPlayer.ts):
- Add retry limit (3) with exponential backoff for NETWORK_ERROR (#27)

Docker (Dockerfile.backend):
- Add playwright install chromium step (#22)
2026-07-05 12:33:26 +00:00

101 lines
2.7 KiB
TypeScript

import { useCallback, useEffect, useRef } from "react"
import Hls from "hls.js"
export function useAudioPlayer() {
const audioRef = useRef<HTMLAudioElement>(null)
const hlsRef = useRef<Hls | null>(null)
const networkRetryCount = useRef(0)
const onStateChange = useRef<((state: "playing" | "paused" | "stopped" | "error") => void) | null>(null)
const play = useCallback((streamUrl: string, streamType: "hls" | "direct" | null) => {
if (!audioRef.current) return
if (hlsRef.current) {
hlsRef.current.destroy()
hlsRef.current = null
}
if (streamType === "hls" && Hls.isSupported()) {
const hls = new Hls({
maxBufferLength: 30,
maxMaxBufferLength: 60,
})
hls.loadSource(streamUrl)
hls.attachMedia(audioRef.current)
hls.on(Hls.Events.MANIFEST_PARSED, () => {
audioRef.current?.play()
onStateChange.current?.("playing")
})
hls.on(Hls.Events.ERROR, (_event, data) => {
if (data.fatal) {
if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
networkRetryCount.current++
if (networkRetryCount.current > 3) {
hls.destroy()
hlsRef.current = null
onStateChange.current?.("error")
} else {
const backoff = Math.min(2 ** networkRetryCount.current * 1000, 10000)
setTimeout(() => hls.startLoad(), backoff)
}
} else {
hls.destroy()
hlsRef.current = null
onStateChange.current?.("error")
}
}
})
networkRetryCount.current = 0
hlsRef.current = hls
} else {
audioRef.current.src = streamUrl
audioRef.current.play().then(() => {
onStateChange.current?.("playing")
}).catch(() => {
onStateChange.current?.("error")
})
}
}, [])
const pause = useCallback(() => {
audioRef.current?.pause()
onStateChange.current?.("paused")
}, [])
const stop = useCallback(() => {
audioRef.current?.pause()
if (hlsRef.current) {
hlsRef.current.destroy()
hlsRef.current = null
}
if (audioRef.current) {
audioRef.current.src = ""
}
onStateChange.current?.("stopped")
}, [])
const setVolume = useCallback((volume: number) => {
if (audioRef.current) {
audioRef.current.volume = volume
}
}, [])
const setStateChange = useCallback((handler: (state: "playing" | "paused" | "stopped" | "error") => void) => {
onStateChange.current = handler
}, [])
useEffect(() => {
return () => {
if (hlsRef.current) {
hlsRef.current.destroy()
hlsRef.current = null
}
}
}, [])
return { audioRef, play, pause, stop, setVolume, setStateChange }
}