- 修复循环播放
This commit is contained in:
+110
-123
@@ -36,137 +36,138 @@ export default function Screen() {
|
||||
const [soundBlocked, setSoundBlocked] = useState(false);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [loaderText, setLoaderText] = useState('加载播放列表中...');
|
||||
const [mediaKey, setMediaKey] = useState(0); // 强制重新渲染,用于单项目循环
|
||||
const [showVideo, setShowVideo] = useState(true);
|
||||
const [currentSrc, setCurrentSrc] = useState('');
|
||||
|
||||
const videoRef = useRef(null);
|
||||
const imgRef = useRef(null);
|
||||
const timerRef = useRef(null);
|
||||
|
||||
// ref 存储可变值,避免回调中的闭包过期
|
||||
const listRef = useRef(list);
|
||||
const indexRef = useRef(index);
|
||||
const pausedRef = useRef(paused);
|
||||
const playModeRef = useRef(playMode);
|
||||
const imageDurationRef = useRef(imageDuration);
|
||||
const soundBlockedRef = useRef(soundBlocked);
|
||||
|
||||
useEffect(() => { listRef.current = list; }, [list]);
|
||||
useEffect(() => { indexRef.current = index; }, [index]);
|
||||
useEffect(() => { pausedRef.current = paused; }, [paused]);
|
||||
useEffect(() => { playModeRef.current = playMode; }, [playMode]);
|
||||
useEffect(() => { imageDurationRef.current = imageDuration; }, [imageDuration]);
|
||||
useEffect(() => { soundBlockedRef.current = soundBlocked; }, [soundBlocked]);
|
||||
|
||||
const getUrl = useCallback((item) => {
|
||||
return item.source === 'url' ? item.relative_path : `/file/${item.relative_path}`;
|
||||
}, []);
|
||||
|
||||
const reportState = useCallback((status) => {
|
||||
const item = list[index] || null;
|
||||
api.updateState({
|
||||
status,
|
||||
index,
|
||||
name: item ? item.name : '',
|
||||
type: item ? item.type : '',
|
||||
}).catch(() => {});
|
||||
}, [list, index]);
|
||||
// ============ 核心播放控制 ============
|
||||
|
||||
const updateStatusUI = useCallback(() => { }, []); // CSS handles visual feedback
|
||||
|
||||
const play = useCallback(() => {
|
||||
/** 稳定的 next — 始终推进索引(不检查 paused),永不停止 */
|
||||
const next = useCallback(() => {
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
if (paused) return;
|
||||
const item = list[index];
|
||||
if (!item) return;
|
||||
const url = getUrl(item);
|
||||
|
||||
if (item.type === 'video') {
|
||||
setShowVideo(true);
|
||||
setCurrentSrc(url);
|
||||
const video = videoRef.current;
|
||||
if (video) {
|
||||
video.style.display = 'block';
|
||||
video.src = url;
|
||||
video.load();
|
||||
video.muted = soundBlocked;
|
||||
const p = video.play();
|
||||
if (p !== undefined) {
|
||||
p.then(() => {
|
||||
reportState('playing');
|
||||
}).catch(() => {
|
||||
setSoundBlocked(true);
|
||||
video.muted = true;
|
||||
video.play().catch(() => {});
|
||||
reportState('playing');
|
||||
});
|
||||
}
|
||||
const len = listRef.current.length;
|
||||
if (len === 0) return;
|
||||
const mode = playModeRef.current;
|
||||
setIndex(prev => {
|
||||
if (mode === 'random') {
|
||||
return Math.floor(Math.random() * len);
|
||||
}
|
||||
} else {
|
||||
// Image
|
||||
setShowVideo(false);
|
||||
setCurrentSrc(url);
|
||||
reportState('playing');
|
||||
timerRef.current = setTimeout(next, imageDuration * 1000);
|
||||
}
|
||||
}, [list, index, paused, getUrl, soundBlocked, imageDuration, reportState]);
|
||||
// 关键:始终循环,(prev + 1) % len 保证永不越界
|
||||
return (prev + 1) % len;
|
||||
});
|
||||
// 单项目时 index 不变((0+1)%1=0),用 mediaKey 强制渲染
|
||||
setMediaKey(k => k + 1);
|
||||
}, []);
|
||||
|
||||
const skip = useCallback((delta) => {
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
const len = listRef.current.length;
|
||||
if (len === 0) return;
|
||||
const mode = playModeRef.current;
|
||||
setIndex(prev => {
|
||||
if (playMode === 'random') {
|
||||
return Math.floor(Math.random() * list.length);
|
||||
if (mode === 'random') {
|
||||
return Math.floor(Math.random() * len);
|
||||
}
|
||||
return (prev + delta + list.length) % list.length;
|
||||
return (prev + delta + len) % len;
|
||||
});
|
||||
}, [playMode, list.length]);
|
||||
setMediaKey(k => k + 1);
|
||||
}, []);
|
||||
|
||||
const next = useCallback(() => {
|
||||
if (paused) return;
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
setIndex(prev => {
|
||||
if (playMode === 'random') {
|
||||
return Math.floor(Math.random() * list.length);
|
||||
}
|
||||
return (prev + 1) % list.length;
|
||||
});
|
||||
}, [paused, playMode, list.length]);
|
||||
// ============ 播放效果 ============
|
||||
|
||||
// Play when index changes
|
||||
// index / mediaKey 变化时播放对应项
|
||||
useEffect(() => {
|
||||
if (loaded && list.length > 0) {
|
||||
const item = list[index];
|
||||
if (item) {
|
||||
if (item.type === 'video') {
|
||||
setShowVideo(true);
|
||||
setCurrentSrc(getUrl(item));
|
||||
} else {
|
||||
const url = getUrl(item);
|
||||
setShowVideo(false);
|
||||
setCurrentSrc(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [index, loaded]);
|
||||
|
||||
// Separate effect for playback triggered by src changes
|
||||
useEffect(() => {
|
||||
if (!loaded || list.length === 0 || !currentSrc) return;
|
||||
if (!loaded || list.length === 0 || paused) return;
|
||||
const item = list[index];
|
||||
if (!item) return;
|
||||
|
||||
// 同步状态到后端
|
||||
api.updateState({
|
||||
status: 'playing',
|
||||
index,
|
||||
name: item.name || '',
|
||||
media_type: item.type || '',
|
||||
}).catch(() => {});
|
||||
|
||||
if (item.type === 'video') {
|
||||
setShowVideo(true);
|
||||
const video = videoRef.current;
|
||||
if (video) {
|
||||
video.style.display = 'block';
|
||||
video.src = currentSrc;
|
||||
video.load();
|
||||
video.muted = soundBlocked;
|
||||
const p = video.play();
|
||||
if (p !== undefined) {
|
||||
p.then(() => reportState('playing'))
|
||||
.catch(() => {
|
||||
setSoundBlocked(true);
|
||||
video.muted = true;
|
||||
video.play().catch(() => {});
|
||||
reportState('playing');
|
||||
});
|
||||
}
|
||||
if (!video) return;
|
||||
const url = getUrl(item);
|
||||
video.src = url;
|
||||
video.load();
|
||||
video.muted = soundBlocked;
|
||||
video.volume = volume;
|
||||
const p = video.play();
|
||||
if (p !== undefined) {
|
||||
p.catch(() => {
|
||||
setSoundBlocked(true);
|
||||
video.muted = true;
|
||||
video.play().catch(() => {});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 图片
|
||||
setShowVideo(false);
|
||||
const url = getUrl(item);
|
||||
imgRef.current.src = url;
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
reportState('playing');
|
||||
timerRef.current = setTimeout(next, imageDuration * 1000);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [index]);
|
||||
}, [index, mediaKey, loaded, paused]);
|
||||
|
||||
// Load playlist on mount
|
||||
// 恢复播放:paused 从 true → false 时触发
|
||||
useEffect(() => {
|
||||
if (paused || !loaded || list.length === 0) return;
|
||||
const item = list[index];
|
||||
if (!item || item.type !== 'video') return;
|
||||
const video = videoRef.current;
|
||||
if (video && video.paused && video.src) {
|
||||
video.play().catch(() => {
|
||||
setSoundBlocked(true);
|
||||
video.muted = true;
|
||||
video.play().catch(() => {});
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [paused]);
|
||||
|
||||
// ============ 事件监听 ============
|
||||
|
||||
// Video ended → next(稳定,永不失效)
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
const handler = () => next();
|
||||
video.addEventListener('ended', handler);
|
||||
return () => video.removeEventListener('ended', handler);
|
||||
// mediaKey 变化时 video 元素被 key 强制重建,需要重新绑定
|
||||
}, [next, mediaKey]);
|
||||
|
||||
// 加载播放列表
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
try {
|
||||
@@ -185,7 +186,7 @@ export default function Screen() {
|
||||
}
|
||||
setLoaded(true);
|
||||
|
||||
// Try play with sound
|
||||
// 尝试启用声音
|
||||
const video = videoRef.current;
|
||||
if (video) {
|
||||
video.muted = false;
|
||||
@@ -194,21 +195,18 @@ export default function Screen() {
|
||||
testPlay.then(() => {
|
||||
setSoundBlocked(false);
|
||||
video.pause();
|
||||
reportState('idle');
|
||||
}).catch(() => {
|
||||
setSoundBlocked(true);
|
||||
video.muted = true;
|
||||
reportState('idle');
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {/* ignore */}
|
||||
};
|
||||
load();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// Connect SSE
|
||||
// SSE 连接
|
||||
useEffect(() => {
|
||||
const es = new EventSource('/api/events');
|
||||
es.onmessage = (e) => {
|
||||
@@ -219,12 +217,10 @@ export default function Screen() {
|
||||
setPaused(true);
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
if (videoRef.current) videoRef.current.pause();
|
||||
reportState('paused');
|
||||
break;
|
||||
case 'play':
|
||||
setPaused(false);
|
||||
play();
|
||||
reportState('playing');
|
||||
// 由 paused → false 的 useEffect 自动恢复播放
|
||||
break;
|
||||
case 'next':
|
||||
setPaused(false);
|
||||
@@ -245,13 +241,13 @@ export default function Screen() {
|
||||
};
|
||||
return () => es.close();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [play, skip, reportState]);
|
||||
}, [skip]);
|
||||
|
||||
const reloadPlaylist = useCallback(async () => {
|
||||
try {
|
||||
const data = await api.getPlaylist();
|
||||
const newList = data.files || [];
|
||||
const currentItem = list[index] || null;
|
||||
const currentItem = listRef.current[indexRef.current] || null;
|
||||
|
||||
if (newList.length === 0) {
|
||||
setList([]);
|
||||
@@ -276,7 +272,7 @@ export default function Screen() {
|
||||
setIndex(prev => Math.min(prev, newList.length - 1));
|
||||
}
|
||||
} catch {/* ignore */}
|
||||
}, [list, index]);
|
||||
}, []);
|
||||
|
||||
const applySettings = useCallback((msg) => {
|
||||
if (msg.volume !== undefined) {
|
||||
@@ -285,7 +281,7 @@ export default function Screen() {
|
||||
const video = videoRef.current;
|
||||
if (video) {
|
||||
video.volume = vol;
|
||||
if (soundBlocked) {
|
||||
if (soundBlockedRef.current) {
|
||||
video.muted = false;
|
||||
video.play().then(() => {
|
||||
setSoundBlocked(false);
|
||||
@@ -300,7 +296,7 @@ export default function Screen() {
|
||||
}
|
||||
if (msg.play_mode !== undefined) setPlayMode(msg.play_mode);
|
||||
if (msg.image_duration !== undefined) setImageDuration(msg.image_duration);
|
||||
}, [soundBlocked]);
|
||||
}, []);
|
||||
|
||||
const enableSound = useCallback(() => {
|
||||
const video = videoRef.current;
|
||||
@@ -316,23 +312,14 @@ export default function Screen() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Video ended handler
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
const handler = () => next();
|
||||
video.addEventListener('ended', handler);
|
||||
return () => video.removeEventListener('ended', handler);
|
||||
}, [next]);
|
||||
|
||||
// Global click for sound unblock
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
if (soundBlocked) enableSound();
|
||||
if (soundBlockedRef.current) enableSound();
|
||||
};
|
||||
document.addEventListener('click', handler);
|
||||
return () => document.removeEventListener('click', handler);
|
||||
}, [soundBlocked, enableSound]);
|
||||
}, [enableSound]);
|
||||
|
||||
// Cleanup timer on unmount
|
||||
useEffect(() => {
|
||||
@@ -355,10 +342,11 @@ export default function Screen() {
|
||||
{/* Loader */}
|
||||
<Loader hidden={loaded && list.length > 0} text={loaderText} />
|
||||
|
||||
{/* Media Players */}
|
||||
{/* Media Players — key={mediaKey} 强制重建以实现单项目循环 */}
|
||||
<video
|
||||
ref={videoRef}
|
||||
id="player"
|
||||
key={mediaKey}
|
||||
autoPlay
|
||||
playsInline
|
||||
controlsList="nodownload"
|
||||
@@ -368,7 +356,6 @@ export default function Screen() {
|
||||
ref={imgRef}
|
||||
id="imgPlayer"
|
||||
alt=""
|
||||
src={!showVideo && loaded && currentItem ? getUrl(currentItem) : undefined}
|
||||
style={{ display: !showVideo && loaded ? 'block' : 'none' }}
|
||||
/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user