Skip to content

Commit b34e23e

Browse files
committed
feat: stabilize audio player and background playback
1 parent d193443 commit b34e23e

5 files changed

Lines changed: 134 additions & 72 deletions

File tree

web/src/app/(main)/live/page.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,16 @@ export default function LivePage() {
1818
const hasCheckedAudio = useRef(false);
1919

2020
useEffect(() => {
21-
if (!hasCheckedAudio.current && isPlaying) {
22-
setShowAudioConfirm(true);
23-
hasCheckedAudio.current = true;
21+
if (isPlaying) {
22+
// If music is playing, we want to show the overlay unless the user
23+
// already acknowledged it for this specific session on this page.
24+
if (!hasCheckedAudio.current) {
25+
setShowAudioConfirm(true);
26+
hasCheckedAudio.current = true;
27+
}
28+
} else {
29+
// If music stops, we can reset the check so it works again if music starts
30+
hasCheckedAudio.current = false;
2431
}
2532
}, [isPlaying]);
2633

web/src/app/layout.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
22
import { Geist, Geist_Mono } from "next/font/google";
33

44
import { LanguageProvider } from "@/context/LanguageContext";
5+
import { Providers } from "@/components/common/Providers";
56
import "./globals.css";
67
import UpdateDetector from "@/components/common/UpdateDetector";
78
import ServiceWorkerRegistration from "@/components/common/ServiceWorkerRegistration";
@@ -119,7 +120,9 @@ export default function RootLayout({
119120
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
120121
>
121122
<LanguageProvider>
122-
{children}
123+
<Providers>
124+
{children}
125+
</Providers>
123126
<UpdateDetector />
124127
<ServiceWorkerRegistration />
125128
<Analytics />
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
'use client';
2+
3+
import { ReactNode } from 'react';
4+
import { AudioProvider } from '@/context/AudioContext';
5+
import { InquiryProvider } from '@/context/InquiryContext';
6+
import audioTracks from '@/data/audio_tracks.json';
7+
8+
interface ProvidersProps {
9+
children: ReactNode;
10+
}
11+
12+
export function Providers({ children }: ProvidersProps) {
13+
return (
14+
<InquiryProvider>
15+
<AudioProvider allTracks={audioTracks as any}>
16+
{children}
17+
</AudioProvider>
18+
</InquiryProvider>
19+
);
20+
}

web/src/components/layout/Layout.tsx

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,24 @@ import Header from './Header';
33
import BottomNav from './BottomNav';
44
import Sidebar from './Sidebar';
55
import { usePathname } from 'next/navigation';
6-
import { AudioProvider } from '@/context/AudioContext';
7-
import { InquiryProvider } from '@/context/InquiryContext';
86
import MiniPlayer from '@/components/audio/MiniPlayer';
9-
import audioTracks from '@/data/audio_tracks.json';
107

118

129
export default function Layout({ children }: { children: React.ReactNode }) {
1310
const pathname = usePathname();
1411
return (
15-
<InquiryProvider>
16-
<AudioProvider allTracks={audioTracks}>
17-
<div className="flex min-h-screen bg-gray-50">
18-
<Sidebar />
19-
<div className="flex-1 flex flex-col lg:ml-64 transition-all duration-300">
20-
<Header />
21-
<main className="flex-1 pt-16 lg:pt-0 pb-32 lg:pb-4 max-w-7xl mx-auto w-full relative">
22-
<div className="lg:px-4 page-fade-in" key={pathname}>
23-
{children}
24-
</div>
25-
</main>
26-
<MiniPlayer />
27-
<BottomNav />
12+
<div className="flex min-h-screen bg-gray-50">
13+
<Sidebar />
14+
<div className="flex-1 flex flex-col lg:ml-64 transition-all duration-300">
15+
<Header />
16+
<main className="flex-1 pt-16 lg:pt-0 pb-32 lg:pb-4 max-w-7xl mx-auto w-full relative">
17+
<div className="lg:px-4 page-fade-in" key={pathname}>
18+
{children}
2819
</div>
29-
</div>
30-
</AudioProvider>
31-
</InquiryProvider>
20+
</main>
21+
<MiniPlayer />
22+
<BottomNav />
23+
</div>
24+
</div>
3225
);
3326
}

web/src/context/AudioContext.tsx

Lines changed: 88 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -32,37 +32,97 @@ export function AudioProvider({ children, allTracks }: { children: React.ReactNo
3232
const [progress, setProgress] = useState(0);
3333
const audioRef = useRef<HTMLAudioElement | null>(null);
3434

35+
// Refs for state to avoid closure issues in event listeners
36+
const currentTrackRef = useRef<Track | null>(null);
37+
const allTracksRef = useRef<Track[]>(allTracks);
38+
39+
useEffect(() => {
40+
currentTrackRef.current = currentTrack;
41+
}, [currentTrack]);
42+
43+
useEffect(() => {
44+
allTracksRef.current = allTracks;
45+
}, [allTracks]);
46+
47+
const playTrack = (track: Track) => {
48+
if (audioRef.current) {
49+
setIsLoading(true);
50+
audioRef.current.src = track.url;
51+
audioRef.current.play().catch(e => {
52+
console.error("Playback failed:", e);
53+
setIsLoading(false);
54+
});
55+
setCurrentTrack(track);
56+
setIsPlaying(true);
57+
}
58+
};
59+
60+
const playNext = () => {
61+
const track = currentTrackRef.current;
62+
const tracks = allTracksRef.current;
63+
if (!track || tracks.length === 0) return;
64+
const currentIndex = tracks.findIndex(t => t.id === track.id);
65+
const nextIndex = (currentIndex + 1) % tracks.length;
66+
playTrack(tracks[nextIndex]);
67+
};
68+
69+
const playPrev = () => {
70+
const track = currentTrackRef.current;
71+
const tracks = allTracksRef.current;
72+
if (!track || tracks.length === 0) return;
73+
const currentIndex = tracks.findIndex(t => t.id === track.id);
74+
const prevIndex = (currentIndex - 1 + tracks.length) % tracks.length;
75+
playTrack(tracks[prevIndex]);
76+
};
77+
3578
// Initialize Audio
3679
useEffect(() => {
37-
audioRef.current = new Audio();
80+
if (!audioRef.current) {
81+
audioRef.current = new Audio();
82+
}
83+
const audio = audioRef.current;
3884

3985
const handleEnded = () => playNext();
4086
const handleTimeUpdate = () => {
41-
if (audioRef.current) {
42-
const p = (audioRef.current.currentTime / audioRef.current.duration) * 100;
43-
setProgress(isNaN(p) ? 0 : p);
44-
}
87+
const p = (audio.currentTime / audio.duration) * 100;
88+
setProgress(isNaN(p) ? 0 : p);
4589
};
4690

4791
const handleWaiting = () => setIsLoading(true);
4892
const handleCanPlay = () => setIsLoading(false);
49-
const handlePlaying = () => setIsLoading(false);
93+
const handlePlaying = () => {
94+
setIsLoading(false);
95+
setIsPlaying(true);
96+
};
97+
const handlePause = () => setIsPlaying(false);
98+
const handlePlay = () => setIsPlaying(true);
5099
const handleLoadStart = () => setIsLoading(true);
100+
const handleError = (e: any) => {
101+
console.error("Audio error:", e);
102+
setIsLoading(false);
103+
setIsPlaying(false);
104+
};
51105

52-
audioRef.current.addEventListener('ended', handleEnded);
53-
audioRef.current.addEventListener('timeupdate', handleTimeUpdate);
54-
audioRef.current.addEventListener('waiting', handleWaiting);
55-
audioRef.current.addEventListener('canplay', handleCanPlay);
56-
audioRef.current.addEventListener('playing', handlePlaying);
57-
audioRef.current.addEventListener('loadstart', handleLoadStart);
106+
audio.addEventListener('ended', handleEnded);
107+
audio.addEventListener('timeupdate', handleTimeUpdate);
108+
audio.addEventListener('waiting', handleWaiting);
109+
audio.addEventListener('canplay', handleCanPlay);
110+
audio.addEventListener('playing', handlePlaying);
111+
audio.addEventListener('pause', handlePause);
112+
audio.addEventListener('play', handlePlay);
113+
audio.addEventListener('loadstart', handleLoadStart);
114+
audio.addEventListener('error', handleError);
58115

59116
return () => {
60-
audioRef.current?.removeEventListener('ended', handleEnded);
61-
audioRef.current?.removeEventListener('timeupdate', handleTimeUpdate);
62-
audioRef.current?.removeEventListener('waiting', handleWaiting);
63-
audioRef.current?.removeEventListener('canplay', handleCanPlay);
64-
audioRef.current?.removeEventListener('playing', handlePlaying);
65-
audioRef.current?.removeEventListener('loadstart', handleLoadStart);
117+
audio.removeEventListener('ended', handleEnded);
118+
audio.removeEventListener('timeupdate', handleTimeUpdate);
119+
audio.removeEventListener('waiting', handleWaiting);
120+
audio.removeEventListener('canplay', handleCanPlay);
121+
audio.removeEventListener('playing', handlePlaying);
122+
audio.removeEventListener('pause', handlePause);
123+
audio.removeEventListener('play', handlePlay);
124+
audio.removeEventListener('loadstart', handleLoadStart);
125+
audio.removeEventListener('error', handleError);
66126
};
67127
}, []);
68128

@@ -84,8 +144,16 @@ export function AudioProvider({ children, allTracks }: { children: React.ReactNo
84144
]
85145
});
86146

87-
navigator.mediaSession.setActionHandler('play', () => togglePlay());
88-
navigator.mediaSession.setActionHandler('pause', () => togglePlay());
147+
navigator.mediaSession.setActionHandler('play', () => {
148+
if (audioRef.current && !isPlaying) {
149+
audioRef.current.play().catch(console.error);
150+
}
151+
});
152+
navigator.mediaSession.setActionHandler('pause', () => {
153+
if (audioRef.current && isPlaying) {
154+
audioRef.current.pause();
155+
}
156+
});
89157
navigator.mediaSession.setActionHandler('previoustrack', () => playPrev());
90158
navigator.mediaSession.setActionHandler('nexttrack', () => playNext());
91159

@@ -98,45 +166,16 @@ export function AudioProvider({ children, allTracks }: { children: React.ReactNo
98166
}, [currentTrack, isPlaying]); // Keep in sync with track and state
99167

100168

101-
const playTrack = (track: Track) => {
102-
if (audioRef.current) {
103-
setIsLoading(true); // Set loading immediately
104-
audioRef.current.src = track.url;
105-
audioRef.current.play().catch(e => {
106-
console.error("Playback failed:", e);
107-
setIsLoading(false);
108-
});
109-
setCurrentTrack(track);
110-
setIsPlaying(true);
111-
}
112-
};
113-
114169
const togglePlay = () => {
115170
if (audioRef.current) {
116171
if (isPlaying) {
117172
audioRef.current.pause();
118-
setIsPlaying(false);
119173
} else {
120174
audioRef.current.play().catch(console.error);
121-
setIsPlaying(true);
122175
}
123176
}
124177
};
125178

126-
const playNext = () => {
127-
if (!currentTrack) return;
128-
const currentIndex = allTracks.findIndex(t => t.id === currentTrack.id);
129-
const nextIndex = (currentIndex + 1) % allTracks.length;
130-
playTrack(allTracks[nextIndex]);
131-
};
132-
133-
const playPrev = () => {
134-
if (!currentTrack) return;
135-
const currentIndex = allTracks.findIndex(t => t.id === currentTrack.id);
136-
const prevIndex = (currentIndex - 1 + allTracks.length) % allTracks.length;
137-
playTrack(allTracks[prevIndex]);
138-
};
139-
140179
const closePlayer = () => {
141180
if (audioRef.current) {
142181
audioRef.current.pause();

0 commit comments

Comments
 (0)