fix: simplify music autoplay for Mac desktop compatibility

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
denshooter
2026-02-16 04:15:30 +01:00
parent 41604e8314
commit 43e9d49620
+33 -48
View File
@@ -7,7 +7,6 @@ import type { MediaItem } from '@/lib/types'
const TAIL_SKIP = 10 const TAIL_SKIP = 10
const CROSSFADE_DURATION = 3 const CROSSFADE_DURATION = 3
const VOLUME = 0.4 const VOLUME = 0.4
const FADE_IN_MS = 2000
export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) { export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) {
const track = tracks[0] ?? null const track = tracks[0] ?? null
@@ -17,11 +16,9 @@ export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) {
const audioB = useRef<HTMLAudioElement>(null) const audioB = useRef<HTMLAudioElement>(null)
const activeRef = useRef<'A' | 'B'>('A') const activeRef = useRef<'A' | 'B'>('A')
const fadingRef = useRef(false) const fadingRef = useRef(false)
const startedRef = useRef(false) const playingRef = useRef(false)
const audibleRef = useRef(false)
const [userMuted, setUserMuted] = useState(false) const [userMuted, setUserMuted] = useState(false)
const [playing, setPlaying] = useState(false)
const getActive = useCallback( const getActive = useCallback(
() => (activeRef.current === 'A' ? audioA.current : audioB.current), () => (activeRef.current === 'A' ? audioA.current : audioB.current),
@@ -66,7 +63,7 @@ export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) {
// Monitor for crossfade // Monitor for crossfade
useEffect(() => { useEffect(() => {
if (!playing || !src) return if (!playingRef.current || !src) return
let id: number let id: number
const tick = () => { const tick = () => {
const a = getActive() const a = getActive()
@@ -79,7 +76,7 @@ export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) {
} }
id = requestAnimationFrame(tick) id = requestAnimationFrame(tick)
return () => cancelAnimationFrame(id) return () => cancelAnimationFrame(id)
}, [playing, src, getActive, crossfade]) })
// Fallback loop // Fallback loop
const handleEnded = useCallback(() => { const handleEnded = useCallback(() => {
@@ -96,56 +93,41 @@ export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) {
if (b && !fadingRef.current && !b.paused) b.volume = vol if (b && !fadingRef.current && !b.paused) b.volume = vol
}, [userMuted, getActive, getInactive]) }, [userMuted, getActive, getInactive])
// ── Fade volume from 0 → VOLUME ────────────────────────────── // ── Ensure playback is running ────────────────────────────────
const fadeIn = useCallback(() => { // Called on any user interaction. Starts audio if not started yet.
if (audibleRef.current) return const ensurePlaying = useCallback(() => {
audibleRef.current = true if (playingRef.current) return
const active = activeRef.current === 'A' ? audioA.current : audioB.current const a = audioA.current
if (!active) return if (!a) return
const t0 = performance.now() a.volume = VOLUME
const step = () => { a.play().then(() => {
const t = Math.min((performance.now() - t0) / FADE_IN_MS, 1) playingRef.current = true
if (!fadingRef.current) active.volume = VOLUME * t }).catch(() => {})
if (t < 1) requestAnimationFrame(step)
}
requestAnimationFrame(step)
}, []) }, [])
// ── Start playback (called directly in gesture handler for mobile) ── // Try autoplay on mount (silent, then make audible on interaction)
const startPlayback = useCallback(() => {
if (startedRef.current) return
const a = audioA.current
if (!a) return
startedRef.current = true
a.volume = 0
a.play().then(() => {
setPlaying(true)
fadeIn()
}).catch(() => { startedRef.current = false })
}, [fadeIn])
// ── Autoplay ─────────────────────────────────────────────────
useEffect(() => { useEffect(() => {
if (!src || startedRef.current) return if (!src) return
const a = audioA.current const a = audioA.current
if (!a) return if (!a) return
// Try silent autoplay (desktop allows this) // Try to autoplay
a.volume = 0 a.volume = VOLUME
a.play().then(() => { a.play().then(() => {
startedRef.current = true playingRef.current = true
setPlaying(true)
// Playing silently — fade in on first interaction
const onInteract = () => fadeIn()
const events = ['scroll', 'click', 'touchstart', 'keydown'] as const
events.forEach((e) => window.addEventListener(e, onInteract, { once: true, passive: true }))
}).catch(() => { }).catch(() => {
// Blocked (mobile) — start on first gesture // Blocked — will start on first interaction via ensurePlaying
const onGesture = () => startPlayback()
const events = ['click', 'touchstart', 'touchend', 'keydown'] as const
events.forEach((e) => document.addEventListener(e, onGesture, { once: true, passive: true }))
}) })
}, [src, fadeIn, startPlayback])
// Safety net: on any interaction, make sure audio is playing
const handler = () => ensurePlaying()
const events = ['click', 'touchstart', 'scroll', 'keydown'] as const
events.forEach((e) => window.addEventListener(e, handler, { passive: true }))
return () => {
events.forEach((e) => window.removeEventListener(e, handler))
}
}, [src, ensurePlaying])
if (!track || !src) return null if (!track || !src) return null
@@ -155,7 +137,10 @@ export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) {
<audio ref={audioB} src={src} preload="auto" onEnded={handleEnded} /> <audio ref={audioB} src={src} preload="auto" onEnded={handleEnded} />
<button <button
onClick={() => setUserMuted((m) => !m)} onClick={() => {
ensurePlaying()
setUserMuted((m) => !m)
}}
className="fixed bottom-6 right-6 z-50 w-14 h-14 rounded-full bg-stone-950/85 backdrop-blur-sm border border-amber-900/20 shadow-lg flex items-center justify-center text-amber-400/60 hover:text-amber-300 transition-colors" className="fixed bottom-6 right-6 z-50 w-14 h-14 rounded-full bg-stone-950/85 backdrop-blur-sm border border-amber-900/20 shadow-lg flex items-center justify-center text-amber-400/60 hover:text-amber-300 transition-colors"
title={userMuted ? 'Ton an' : 'Ton aus'} title={userMuted ? 'Ton an' : 'Ton aus'}
> >