修复:CI 部署环境与 ref 错配、迁移碎片化、图片意图 source_span、章节物化脏版式、会话历史与本地语音不一致
新增:TTS 上传 COS 与分片、章节 reading_segments 物化与快照、markdown 清洗、会话消息 repository、语音 store 重构与相关测试
This commit is contained in:
@@ -8,8 +8,11 @@ import type { PlaybackItem, PlayerStatus } from '../types';
|
||||
interface UsePlayerResult {
|
||||
status: PlayerStatus;
|
||||
queueLength: number;
|
||||
enqueueTtsAudio: (audioBase64: string) => void;
|
||||
/** Current playback source URI (file, https, or data URL). */
|
||||
currentSource: string | null;
|
||||
enqueue: (item: PlaybackItem) => void;
|
||||
/** Replace queue and play this item (e.g. user voice bubble vs other sources). */
|
||||
enqueueExclusive: (item: PlaybackItem) => Promise<void>;
|
||||
stop: () => void;
|
||||
}
|
||||
|
||||
@@ -29,8 +32,12 @@ export function usePlayer(): UsePlayerResult {
|
||||
const isPlayingRef = useRef(false);
|
||||
const wasBlockedByRecorderRef = useRef(false);
|
||||
const isPlayNextInProgressRef = useRef(false);
|
||||
/** 同步反映「当前是否正在播放某条 URI」;enqueue 不能依赖 state,否则 await stop() 后仍为陈旧闭包。 */
|
||||
const playbackActiveUriRef = useRef<string | null>(null);
|
||||
/** 当前 source 是否已进入过 playing=true,避免换源瞬间 playerStatus 仍带上一首的 duration 而误判「已播完」。 */
|
||||
const trackHasPlayedRef = useRef(false);
|
||||
|
||||
const player = useAudioPlayer(currentSource);
|
||||
const player = useAudioPlayer(currentSource, { downloadFirst: false });
|
||||
const playerStatus = useAudioPlayerStatus(player);
|
||||
|
||||
// Start playback when a new source is set
|
||||
@@ -46,6 +53,7 @@ export function usePlayer(): UsePlayerResult {
|
||||
isPlayNextInProgressRef.current = true;
|
||||
try {
|
||||
if (queueRef.current.length === 0) {
|
||||
playbackActiveUriRef.current = null;
|
||||
setCurrentSource(null);
|
||||
setStatus('idle');
|
||||
setQueueLength(0);
|
||||
@@ -64,20 +72,33 @@ export function usePlayer(): UsePlayerResult {
|
||||
const next = queueRef.current.shift()!;
|
||||
setQueueLength(queueRef.current.length);
|
||||
setStatus('playing');
|
||||
trackHasPlayedRef.current = false;
|
||||
playbackActiveUriRef.current = next.uri;
|
||||
setCurrentSource(next.uri);
|
||||
} finally {
|
||||
isPlayNextInProgressRef.current = false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Detect playback completion → advance queue
|
||||
useEffect(() => {
|
||||
if (playerStatus.playing) {
|
||||
trackHasPlayedRef.current = true;
|
||||
}
|
||||
}, [playerStatus.playing]);
|
||||
|
||||
// Detect playback completion → advance queue(必须曾 playing,避免换源瞬间沿用上一条的 duration/currentTime)
|
||||
useEffect(() => {
|
||||
if (!currentSource || !isPlayingRef.current) return;
|
||||
|
||||
const { playing, currentTime, duration } = playerStatus;
|
||||
const finished = !playing && duration > 0 && currentTime >= duration - 0.05;
|
||||
const finished =
|
||||
trackHasPlayedRef.current &&
|
||||
!playing &&
|
||||
duration > 0 &&
|
||||
currentTime >= duration - 0.05;
|
||||
|
||||
if (finished) {
|
||||
trackHasPlayedRef.current = false;
|
||||
isPlayingRef.current = false;
|
||||
playNext();
|
||||
}
|
||||
@@ -107,19 +128,31 @@ export function usePlayer(): UsePlayerResult {
|
||||
queueRef.current.push(item);
|
||||
setQueueLength(queueRef.current.length);
|
||||
|
||||
if (status === 'idle' && !currentSource) {
|
||||
const shouldKick =
|
||||
queueRef.current.length === 1 && playbackActiveUriRef.current === null;
|
||||
|
||||
if (shouldKick) {
|
||||
await playNext();
|
||||
}
|
||||
},
|
||||
[status, currentSource, playNext],
|
||||
[playNext],
|
||||
);
|
||||
|
||||
const enqueueTtsAudio = useCallback(
|
||||
(audioBase64: string) => {
|
||||
const uri = `data:audio/mp3;base64,${audioBase64}`;
|
||||
enqueue({ uri, label: 'TTS' });
|
||||
const enqueueExclusive = useCallback(
|
||||
async (item: PlaybackItem) => {
|
||||
queueRef.current = [item];
|
||||
setQueueLength(1);
|
||||
isPlayingRef.current = false;
|
||||
if (player) {
|
||||
player.pause();
|
||||
}
|
||||
playbackActiveUriRef.current = null;
|
||||
setCurrentSource(null);
|
||||
setStatus('idle');
|
||||
await audioFocus.release();
|
||||
await playNext();
|
||||
},
|
||||
[enqueue],
|
||||
[player, playNext],
|
||||
);
|
||||
|
||||
const stop = useCallback(async () => {
|
||||
@@ -131,10 +164,18 @@ export function usePlayer(): UsePlayerResult {
|
||||
player.pause();
|
||||
}
|
||||
|
||||
playbackActiveUriRef.current = null;
|
||||
setCurrentSource(null);
|
||||
setStatus('idle');
|
||||
await audioFocus.release();
|
||||
}, [player]);
|
||||
|
||||
return { status, queueLength, enqueueTtsAudio, enqueue, stop };
|
||||
return {
|
||||
status,
|
||||
queueLength,
|
||||
currentSource,
|
||||
enqueue,
|
||||
enqueueExclusive,
|
||||
stop,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user