Skip to content

Commit 0cbd41f

Browse files
authored
Merge pull request #137 from Rindrics/issue-136-stop-audio
fix: stop audio on navigation
2 parents 47e3d62 + f86879d commit 0cbd41f

2 files changed

Lines changed: 32 additions & 1 deletion

File tree

models/AudioPlayer.tla

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,13 @@ PlaySingleSegment(seg) ==
156156
/\ currentSegment' = seg
157157
/\ loopEnabled' = TRUE
158158

159+
\* アンマウント (ページ遷移時のクリーンアップ)
160+
\* どの状態からでも発生し、再生を強制停止する
161+
Unmount ==
162+
/\ playState' = "stopped"
163+
/\ currentSegment' = 0
164+
/\ UNCHANGED <<loopEnabled, selectedSegments>>
165+
159166
-----------------------------------------------------------------------------
160167
(* 状態遷移 *)
161168
Next ==
@@ -167,6 +174,7 @@ Next ==
167174
\/ SelectAll
168175
\/ SelectNone
169176
\/ \E seg \in Segments : PlaySingleSegment(seg)
177+
\/ Unmount
170178

171179
-----------------------------------------------------------------------------
172180
(* 時間的性質 *)

src/components/AudioPlayer.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ export function AudioPlayer({
6666
const playSegmentRef = useRef<((segmentIndex: number) => void) | null>(null);
6767
const onEndedRef = useRef<(() => void) | null>(null);
6868
const onErrorRef = useRef<(() => void) | null>(null);
69+
const resumeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
70+
const isMountedRef = useRef(true);
6971

7072
// Reset when content changes
7173
// biome-ignore lint/correctness/useExhaustiveDependencies: contentId change should reset state
@@ -122,6 +124,19 @@ export function AudioPlayer({
122124
onErrorRef.current = null;
123125
}, []);
124126

127+
// Stop audio on unmount (e.g. page navigation)
128+
useEffect(() => {
129+
isMountedRef.current = true;
130+
return () => {
131+
isMountedRef.current = false;
132+
cleanupAudio();
133+
if (resumeTimeoutRef.current !== null) {
134+
clearTimeout(resumeTimeoutRef.current);
135+
resumeTimeoutRef.current = null;
136+
}
137+
};
138+
}, [cleanupAudio]);
139+
125140
// Play a specific segment
126141
const playSegment = useCallback(
127142
(segmentIndex: number) => {
@@ -140,6 +155,7 @@ export function AudioPlayer({
140155
audioRef.current = audio;
141156

142157
const onEnded = () => {
158+
if (!isMountedRef.current) return;
143159
const next = getNextSelectedSegment(segmentIndex);
144160
if (next !== null) {
145161
setCurrentSegment(next);
@@ -160,6 +176,7 @@ export function AudioPlayer({
160176
};
161177

162178
const onError = () => {
179+
if (!isMountedRef.current) return;
163180
setIsPlaying(false);
164181
setCurrentSegment(null);
165182
};
@@ -171,6 +188,7 @@ export function AudioPlayer({
171188
audio.addEventListener('error', onError);
172189

173190
audio.play().catch((error) => {
191+
if (!isMountedRef.current) return;
174192
console.error('Failed to play audio:', error);
175193
setIsPlaying(false);
176194
setCurrentSegment(null);
@@ -226,7 +244,12 @@ export function AudioPlayer({
226244

227245
// Resume playing if it was playing before
228246
if (wasPlaying && wasSegment !== null) {
229-
setTimeout(() => {
247+
if (resumeTimeoutRef.current !== null) {
248+
clearTimeout(resumeTimeoutRef.current);
249+
}
250+
resumeTimeoutRef.current = setTimeout(() => {
251+
resumeTimeoutRef.current = null;
252+
if (!isMountedRef.current) return;
230253
setIsPlaying(true);
231254
playSegment(wasSegment);
232255
}, 100);

0 commit comments

Comments
 (0)