fix(expo): 暂停自动朗读后继续播放最新 TTS 片段
- usePlayer:paused 且 tts_auto 时清空队列并重置,再播当前片段 - 用 statusRef 与暂停同步,避免 WS 紧连 enqueue 时状态滞后 - 补充 use-player 单测 - api: 调整 copyright_source_pdf 脚本 - docs: 新增软著《岁月时书》软件设计说明书 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -40,6 +40,8 @@ export function usePlayer(): UsePlayerResult {
|
||||
const isPlayingRef = useRef(false);
|
||||
const wasBlockedByRecorderRef = useRef(false);
|
||||
const isPlayNextInProgressRef = useRef(false);
|
||||
/** 与 `status` 同步;`pausePlayback` 等在同一事件循环内立即更新,避免 WS 紧跟着 `enqueue(tts_auto)` 时读到陈旧 `playing`。 */
|
||||
const statusRef = useRef<PlayerStatus>('idle');
|
||||
/** 同步反映「当前是否正在播放某条 URI」;enqueue 不能依赖 state,否则 await stop() 后仍为陈旧闭包。 */
|
||||
const playbackActiveUriRef = useRef<string | null>(null);
|
||||
/** 当前 source 是否已进入过 playing=true,避免换源瞬间 playerStatus 仍带上一首的 duration 而误判「已播完」。 */
|
||||
@@ -63,6 +65,10 @@ export function usePlayer(): UsePlayerResult {
|
||||
const player = useAudioPlayer(currentSource, playerOptions);
|
||||
const playerStatus = useAudioPlayerStatus(player);
|
||||
|
||||
useEffect(() => {
|
||||
statusRef.current = status;
|
||||
}, [status]);
|
||||
|
||||
/**
|
||||
* 必须在 `isLoaded` 之后再 `play()`。
|
||||
* expo-audio 在 `downloadFirst: true` 时先用 null 建 player,再在内部 effect 里异步
|
||||
@@ -86,6 +92,7 @@ export function usePlayer(): UsePlayerResult {
|
||||
playbackActiveUriRef.current = null;
|
||||
setCurrentPlaybackItem(null);
|
||||
setCurrentSource(null);
|
||||
statusRef.current = 'idle';
|
||||
setStatus('idle');
|
||||
setQueueLength(0);
|
||||
await audioFocus.releaseIfOwnedBy('player');
|
||||
@@ -100,6 +107,7 @@ export function usePlayer(): UsePlayerResult {
|
||||
* `wasBlockedByRecorderRef` 不会被置位,录音结束后也不会重试 playNext。
|
||||
*/
|
||||
wasBlockedByRecorderRef.current = true;
|
||||
statusRef.current = 'idle';
|
||||
setStatus('idle');
|
||||
return;
|
||||
}
|
||||
@@ -108,6 +116,7 @@ export function usePlayer(): UsePlayerResult {
|
||||
|
||||
const next = queueRef.current.shift()!;
|
||||
setQueueLength(queueRef.current.length);
|
||||
statusRef.current = 'playing';
|
||||
setStatus('playing');
|
||||
trackHasPlayedRef.current = false;
|
||||
playbackActiveUriRef.current = next.uri;
|
||||
@@ -150,6 +159,7 @@ export function usePlayer(): UsePlayerResult {
|
||||
player.pause();
|
||||
}
|
||||
isPlayingRef.current = false;
|
||||
statusRef.current = 'paused';
|
||||
return 'paused';
|
||||
});
|
||||
}, [player]);
|
||||
@@ -158,12 +168,14 @@ export function usePlayer(): UsePlayerResult {
|
||||
if (status !== 'paused') return;
|
||||
const acquired = await audioFocus.acquireForPlayback();
|
||||
if (!acquired) {
|
||||
statusRef.current = 'idle';
|
||||
setStatus('idle');
|
||||
return;
|
||||
}
|
||||
if (!player) return;
|
||||
if (!playerStatus.isLoaded) return;
|
||||
player.play();
|
||||
statusRef.current = 'playing';
|
||||
setStatus('playing');
|
||||
isPlayingRef.current = true;
|
||||
}, [status, player, playerStatus.isLoaded]);
|
||||
@@ -192,6 +204,26 @@ export function usePlayer(): UsePlayerResult {
|
||||
|
||||
const enqueue = useCallback(
|
||||
async (item: PlaybackItem) => {
|
||||
/**
|
||||
* 用户在助手自动朗读中途点暂停时,`playbackActiveUriRef` 仍指向当前条,
|
||||
* 后续 `tts_auto` 只会堆在队列里且不会 `playNext`。
|
||||
* 新片段到达时表示「最新已生成」:清掉暂停态与积压队列,只播本条。
|
||||
*/
|
||||
if (item.kind === 'tts_auto' && statusRef.current === 'paused') {
|
||||
queueRef.current = [];
|
||||
setQueueLength(0);
|
||||
isPlayingRef.current = false;
|
||||
if (player) {
|
||||
player.pause();
|
||||
}
|
||||
playbackActiveUriRef.current = null;
|
||||
setCurrentPlaybackItem(null);
|
||||
setCurrentSource(null);
|
||||
statusRef.current = 'idle';
|
||||
setStatus('idle');
|
||||
await audioFocus.releaseIfOwnedBy('player');
|
||||
}
|
||||
|
||||
queueRef.current.push(item);
|
||||
setQueueLength(queueRef.current.length);
|
||||
|
||||
@@ -202,7 +234,7 @@ export function usePlayer(): UsePlayerResult {
|
||||
await playNext();
|
||||
}
|
||||
},
|
||||
[playNext],
|
||||
[playNext, player],
|
||||
);
|
||||
|
||||
const enqueueExclusive = useCallback(
|
||||
@@ -216,6 +248,7 @@ export function usePlayer(): UsePlayerResult {
|
||||
playbackActiveUriRef.current = null;
|
||||
setCurrentPlaybackItem(null);
|
||||
setCurrentSource(null);
|
||||
statusRef.current = 'idle';
|
||||
setStatus('idle');
|
||||
await audioFocus.releaseIfOwnedBy('player');
|
||||
await playNext();
|
||||
@@ -235,6 +268,7 @@ export function usePlayer(): UsePlayerResult {
|
||||
playbackActiveUriRef.current = null;
|
||||
setCurrentPlaybackItem(null);
|
||||
setCurrentSource(null);
|
||||
statusRef.current = 'idle';
|
||||
setStatus('idle');
|
||||
await audioFocus.releaseIfOwnedBy('player');
|
||||
}, [player]);
|
||||
|
||||
@@ -167,4 +167,44 @@ describe('usePlayer', () => {
|
||||
expect(acquire).toHaveBeenCalledTimes(2);
|
||||
expect(play).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('after pause, new tts_auto clears backlog and kicks playNext', async () => {
|
||||
mockUseAudioPlayerStatus.mockReturnValue({
|
||||
isLoaded: true,
|
||||
playing: false,
|
||||
currentTime: 0.1,
|
||||
duration: 10,
|
||||
});
|
||||
const pause = jest.fn();
|
||||
const play = jest.fn();
|
||||
mockUseAudioPlayer.mockReturnValue({ pause, play });
|
||||
|
||||
const { result } = renderHook(() => usePlayer());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.enqueue({
|
||||
uri: 'file:///first.mp3',
|
||||
kind: 'tts_auto',
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.status).toBe('playing');
|
||||
const playCountAfterFirst = play.mock.calls.length;
|
||||
|
||||
act(() => {
|
||||
result.current.pausePlayback();
|
||||
});
|
||||
expect(result.current.status).toBe('paused');
|
||||
|
||||
await act(async () => {
|
||||
await result.current.enqueue({
|
||||
uri: 'file:///latest.mp3',
|
||||
kind: 'tts_auto',
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.status).toBe('playing');
|
||||
expect(play.mock.calls.length).toBeGreaterThan(playCountAfterFirst);
|
||||
expect(result.current.currentSource).toBe('file:///latest.mp3');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user