493 lines
16 KiB
TypeScript
493 lines
16 KiB
TypeScript
// React Hook for Play Page Synchronization
|
|
'use client';
|
|
|
|
import { useEffect, useRef, useCallback } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { useWatchRoomContextSafe } from '@/components/WatchRoomProvider';
|
|
import type { PlayState } from '@/types/watch-room';
|
|
|
|
interface UsePlaySyncOptions {
|
|
artPlayerRef: React.MutableRefObject<any>;
|
|
videoId: string;
|
|
videoName: string;
|
|
videoYear?: string;
|
|
searchTitle?: string;
|
|
currentEpisode?: number;
|
|
currentSource: string;
|
|
videoUrl: string;
|
|
playerReady: boolean; // 播放器是否就绪
|
|
}
|
|
|
|
export function usePlaySync({
|
|
artPlayerRef,
|
|
videoId,
|
|
videoName,
|
|
videoYear,
|
|
searchTitle,
|
|
currentEpisode,
|
|
currentSource,
|
|
videoUrl,
|
|
playerReady,
|
|
}: UsePlaySyncOptions) {
|
|
const router = useRouter();
|
|
const watchRoom = useWatchRoomContextSafe();
|
|
const lastSyncTimeRef = useRef(0); // 上次同步时间
|
|
const isHandlingRemoteCommandRef = useRef(false); // 标记是否正在处理远程命令
|
|
|
|
// 检查是否在房间内
|
|
const isInRoom = !!(watchRoom && watchRoom.currentRoom);
|
|
const isOwner = watchRoom?.isOwner || false;
|
|
const currentRoom = watchRoom?.currentRoom;
|
|
const socket = watchRoom?.socket;
|
|
|
|
// 广播播放状态给房间内所有人(任何成员都可以触发同步)
|
|
const broadcastPlayState = useCallback(() => {
|
|
if (!socket || !watchRoom || !isInRoom) return;
|
|
|
|
const player = artPlayerRef.current;
|
|
if (!player) return;
|
|
|
|
const state: PlayState = {
|
|
type: 'play',
|
|
url: videoUrl,
|
|
currentTime: player.currentTime || 0,
|
|
isPlaying: player.playing || false,
|
|
videoId,
|
|
videoName,
|
|
videoYear,
|
|
searchTitle,
|
|
episode: currentEpisode,
|
|
source: currentSource,
|
|
};
|
|
|
|
// 使用防抖,避免频繁发送
|
|
const now = Date.now();
|
|
if (now - lastSyncTimeRef.current < 1000) return;
|
|
lastSyncTimeRef.current = now;
|
|
|
|
watchRoom.updatePlayState(state);
|
|
}, [socket, videoUrl, videoId, videoName, videoYear, searchTitle, currentEpisode, currentSource, watchRoom, artPlayerRef, isInRoom]);
|
|
|
|
// 接收并同步其他成员的播放状态
|
|
useEffect(() => {
|
|
if (!socket || !currentRoom || !isInRoom) {
|
|
console.log('[PlaySync] Skip setup:', { hasSocket: !!socket, hasRoom: !!currentRoom, isInRoom });
|
|
return;
|
|
}
|
|
|
|
console.log('[PlaySync] Setting up event listeners');
|
|
|
|
const handlePlayUpdate = (state: PlayState) => {
|
|
console.log('[PlaySync] Received play:update event:', state);
|
|
const player = artPlayerRef.current;
|
|
|
|
if (!player) {
|
|
console.warn('[PlaySync] Player not ready for play:update');
|
|
return;
|
|
}
|
|
|
|
console.log('[PlaySync] Processing play update - current state:', {
|
|
playerPlaying: player.playing,
|
|
statePlaying: state.isPlaying,
|
|
playerTime: player.currentTime,
|
|
stateTime: state.currentTime
|
|
});
|
|
|
|
// 标记正在处理远程命令
|
|
isHandlingRemoteCommandRef.current = true;
|
|
|
|
// play:update 只同步进度,不改变播放/暂停状态
|
|
// 播放/暂停状态由 play:play 和 play:pause 命令控制
|
|
const timeDiff = Math.abs(player.currentTime - state.currentTime);
|
|
if (timeDiff > 2) {
|
|
console.log('[PlaySync] Seeking to:', state.currentTime, '(diff:', timeDiff, 's)');
|
|
player.currentTime = state.currentTime;
|
|
// 延迟重置标记,确保 seeked 事件已处理完毕
|
|
setTimeout(() => {
|
|
isHandlingRemoteCommandRef.current = false;
|
|
console.log('[PlaySync] Reset flag after seek');
|
|
}, 500);
|
|
} else {
|
|
console.log('[PlaySync] Time diff is small, no seek needed');
|
|
// 没有操作,立即重置标记
|
|
isHandlingRemoteCommandRef.current = false;
|
|
}
|
|
};
|
|
|
|
const handlePlayCommand = () => {
|
|
console.log('[PlaySync] ========== Received play:play event ==========');
|
|
console.log('[PlaySync] isHandlingRemoteCommandRef:', isHandlingRemoteCommandRef.current);
|
|
const player = artPlayerRef.current;
|
|
|
|
if (!player) {
|
|
console.warn('[PlaySync] Player not ready for play:play');
|
|
return;
|
|
}
|
|
|
|
console.log('[PlaySync] Player state before play:', {
|
|
playing: player.playing,
|
|
currentTime: player.currentTime,
|
|
readyState: player.video?.readyState,
|
|
});
|
|
|
|
// 标记正在处理远程命令
|
|
isHandlingRemoteCommandRef.current = true;
|
|
console.log('[PlaySync] Set flag to true');
|
|
|
|
// 只有在暂停状态时才执行播放
|
|
if (!player.playing) {
|
|
console.log('[PlaySync] Executing play command - calling player.play()');
|
|
player.play()
|
|
.then(() => {
|
|
console.log('[PlaySync] Play command completed successfully');
|
|
console.log('[PlaySync] Player state after play:', {
|
|
playing: player.playing,
|
|
currentTime: player.currentTime,
|
|
});
|
|
// 等待播放器事件触发后再重置标记
|
|
setTimeout(() => {
|
|
isHandlingRemoteCommandRef.current = false;
|
|
console.log('[PlaySync] Reset flag after play');
|
|
}, 500);
|
|
})
|
|
.catch((err: any) => {
|
|
console.error('[PlaySync] Play error:', err);
|
|
isHandlingRemoteCommandRef.current = false;
|
|
});
|
|
} else {
|
|
console.log('[PlaySync] Player already playing, skipping');
|
|
isHandlingRemoteCommandRef.current = false;
|
|
}
|
|
console.log('[PlaySync] ========== End play:play handling ==========');
|
|
};
|
|
|
|
const handlePauseCommand = () => {
|
|
console.log('[PlaySync] ========== Received play:pause event ==========');
|
|
console.log('[PlaySync] isHandlingRemoteCommandRef:', isHandlingRemoteCommandRef.current);
|
|
const player = artPlayerRef.current;
|
|
|
|
if (!player) {
|
|
console.warn('[PlaySync] Player not ready for play:pause');
|
|
return;
|
|
}
|
|
|
|
console.log('[PlaySync] Player state before pause:', {
|
|
playing: player.playing,
|
|
currentTime: player.currentTime,
|
|
});
|
|
|
|
// 标记正在处理远程命令
|
|
isHandlingRemoteCommandRef.current = true;
|
|
console.log('[PlaySync] Set flag to true');
|
|
|
|
// 只有在播放状态时才执行暂停
|
|
if (player.playing) {
|
|
console.log('[PlaySync] Executing pause command - calling player.pause()');
|
|
player.pause();
|
|
console.log('[PlaySync] Player state after pause:', {
|
|
playing: player.playing,
|
|
currentTime: player.currentTime,
|
|
});
|
|
// pause 是同步的,但还是延迟重置以确保事件处理完毕
|
|
setTimeout(() => {
|
|
isHandlingRemoteCommandRef.current = false;
|
|
console.log('[PlaySync] Reset flag after pause');
|
|
}, 500);
|
|
} else {
|
|
console.log('[PlaySync] Player already paused, skipping');
|
|
isHandlingRemoteCommandRef.current = false;
|
|
}
|
|
console.log('[PlaySync] ========== End play:pause handling ==========');
|
|
};
|
|
|
|
const handleSeekCommand = (currentTime: number) => {
|
|
console.log('[PlaySync] Received play:seek event:', currentTime);
|
|
const player = artPlayerRef.current;
|
|
|
|
if (!player) {
|
|
console.warn('[PlaySync] Player not ready for play:seek');
|
|
return;
|
|
}
|
|
|
|
// 标记正在处理远程命令
|
|
isHandlingRemoteCommandRef.current = true;
|
|
|
|
console.log('[PlaySync] Executing seek command');
|
|
player.currentTime = currentTime;
|
|
|
|
// 延迟重置标记,确保 seeked 事件已处理完毕
|
|
setTimeout(() => {
|
|
isHandlingRemoteCommandRef.current = false;
|
|
console.log('[PlaySync] Reset flag after seek command');
|
|
}, 500);
|
|
};
|
|
|
|
const handleChangeCommand = (state: PlayState) => {
|
|
console.log('[PlaySync] Received play:change event:', state);
|
|
console.log('[PlaySync] Current isOwner:', isOwner);
|
|
|
|
// 只有房员才处理视频切换命令
|
|
if (isOwner) {
|
|
console.log('[PlaySync] Skipping play:change - user is owner');
|
|
return;
|
|
}
|
|
|
|
// 跟随切换视频
|
|
// 构建完整的 URL 参数
|
|
const params = new URLSearchParams({
|
|
id: state.videoId,
|
|
source: state.source,
|
|
episode: String(state.episode || 1),
|
|
});
|
|
|
|
// 添加可选参数
|
|
if (state.videoName) params.set('title', state.videoName);
|
|
if (state.videoYear) params.set('year', state.videoYear);
|
|
if (state.searchTitle) params.set('stitle', state.searchTitle);
|
|
|
|
const url = `/play?${params.toString()}`;
|
|
console.log('[PlaySync] Member redirecting to:', url);
|
|
|
|
// 使用 router.push 进行导航,支持在同一页面更新参数
|
|
router.push(url);
|
|
};
|
|
|
|
socket.on('play:update', handlePlayUpdate);
|
|
socket.on('play:play', handlePlayCommand);
|
|
socket.on('play:pause', handlePauseCommand);
|
|
socket.on('play:seek', handleSeekCommand);
|
|
socket.on('play:change', handleChangeCommand);
|
|
|
|
console.log('[PlaySync] Event listeners registered');
|
|
|
|
return () => {
|
|
console.log('[PlaySync] Cleaning up event listeners');
|
|
socket.off('play:update', handlePlayUpdate);
|
|
socket.off('play:play', handlePlayCommand);
|
|
socket.off('play:pause', handlePauseCommand);
|
|
socket.off('play:seek', handleSeekCommand);
|
|
socket.off('play:change', handleChangeCommand);
|
|
};
|
|
}, [socket, currentRoom, isInRoom, isOwner]);
|
|
|
|
// 监听播放器事件并广播(所有成员都可以触发同步)
|
|
useEffect(() => {
|
|
if (!socket || !currentRoom || !isInRoom || !watchRoom) {
|
|
console.log('[PlaySync] Skip player setup:', { hasSocket: !!socket, hasRoom: !!currentRoom, isInRoom, hasWatchRoom: !!watchRoom });
|
|
return;
|
|
}
|
|
|
|
if (!playerReady) {
|
|
console.log('[PlaySync] Player not ready yet, waiting...');
|
|
return;
|
|
}
|
|
|
|
const player = artPlayerRef.current;
|
|
if (!player) {
|
|
console.warn('[PlaySync] Player ref is null despite playerReady=true');
|
|
return;
|
|
}
|
|
|
|
console.log('[PlaySync] Setting up player event listeners');
|
|
|
|
const handlePlay = () => {
|
|
// 如果正在处理远程命令,不要广播(避免循环)
|
|
if (isHandlingRemoteCommandRef.current) {
|
|
console.log('[PlaySync] Play event triggered by remote command, not broadcasting');
|
|
return;
|
|
}
|
|
|
|
const player = artPlayerRef.current;
|
|
if (!player) return;
|
|
|
|
// 确认播放器确实在播放状态才广播
|
|
if (player.playing) {
|
|
console.log('[PlaySync] Play event detected, player is playing, broadcasting...');
|
|
// 只发送 play 命令,不发送完整状态(避免重复)
|
|
watchRoom.play();
|
|
} else {
|
|
console.log('[PlaySync] Play event detected but player is paused, not broadcasting');
|
|
}
|
|
};
|
|
|
|
const handlePause = () => {
|
|
// 如果正在处理远程命令,不要广播(避免循环)
|
|
if (isHandlingRemoteCommandRef.current) {
|
|
console.log('[PlaySync] Pause event triggered by remote command, not broadcasting');
|
|
return;
|
|
}
|
|
|
|
const player = artPlayerRef.current;
|
|
if (!player) return;
|
|
|
|
// 确认播放器确实在暂停状态才广播
|
|
if (!player.playing) {
|
|
console.log('[PlaySync] Pause event detected, player is paused, broadcasting...');
|
|
// 只发送 pause 命令,不发送完整状态(避免重复)
|
|
watchRoom.pause();
|
|
} else {
|
|
console.log('[PlaySync] Pause event detected but player is playing, not broadcasting');
|
|
}
|
|
};
|
|
|
|
const handleSeeked = () => {
|
|
// 如果正在处理远程命令,不要广播(避免循环)
|
|
if (isHandlingRemoteCommandRef.current) {
|
|
console.log('[PlaySync] Seeked event triggered by remote command, not broadcasting');
|
|
return;
|
|
}
|
|
|
|
const player = artPlayerRef.current;
|
|
if (!player) return;
|
|
|
|
console.log('[PlaySync] Seeked event detected, broadcasting time:', player.currentTime);
|
|
watchRoom.seekPlayback(player.currentTime);
|
|
};
|
|
|
|
player.on('play', handlePlay);
|
|
player.on('pause', handlePause);
|
|
player.on('seeked', handleSeeked);
|
|
|
|
// 定期同步播放进度(每5秒)
|
|
const syncInterval = setInterval(() => {
|
|
if (!player.playing) return; // 暂停时不同步
|
|
|
|
console.log('[PlaySync] Periodic sync - broadcasting state');
|
|
broadcastPlayState();
|
|
}, 5000);
|
|
|
|
console.log('[PlaySync] Player event listeners registered with periodic sync');
|
|
|
|
return () => {
|
|
console.log('[PlaySync] Cleaning up player event listeners');
|
|
player.off('play', handlePlay);
|
|
player.off('pause', handlePause);
|
|
player.off('seeked', handleSeeked);
|
|
clearInterval(syncInterval);
|
|
};
|
|
}, [socket, currentRoom, artPlayerRef, watchRoom, broadcastPlayState, isInRoom, playerReady]);
|
|
|
|
// 使用ref跟踪上一次的值,用于检测真正的变化
|
|
const lastBroadcastRef = useRef<{
|
|
videoId: string;
|
|
source: string;
|
|
episode: number;
|
|
} | null>(null);
|
|
|
|
// 房主:监听视频/集数/源变化并广播
|
|
useEffect(() => {
|
|
if (!isOwner || !socket || !currentRoom || !isInRoom || !watchRoom) {
|
|
// 如果不是房主或不在房间,重置跟踪
|
|
lastBroadcastRef.current = null;
|
|
return;
|
|
}
|
|
if (!videoId || !videoUrl) return;
|
|
|
|
const currentState = {
|
|
videoId,
|
|
source: currentSource,
|
|
episode: currentEpisode || 1,
|
|
};
|
|
|
|
// 检查是否需要广播
|
|
const shouldBroadcast = !lastBroadcastRef.current ||
|
|
lastBroadcastRef.current.videoId !== currentState.videoId ||
|
|
lastBroadcastRef.current.source !== currentState.source ||
|
|
lastBroadcastRef.current.episode !== currentState.episode;
|
|
|
|
if (!shouldBroadcast) {
|
|
console.log('[PlaySync] No change detected, skipping broadcast');
|
|
return;
|
|
}
|
|
|
|
console.log('[PlaySync] Detected change, will broadcast:', {
|
|
from: lastBroadcastRef.current,
|
|
to: currentState
|
|
});
|
|
|
|
// 延迟广播,确保页面已经稳定
|
|
const timer = setTimeout(() => {
|
|
const state: PlayState = {
|
|
type: 'play',
|
|
url: videoUrl,
|
|
currentTime: artPlayerRef.current?.currentTime || 0,
|
|
isPlaying: artPlayerRef.current?.playing || false,
|
|
videoId,
|
|
videoName,
|
|
videoYear,
|
|
searchTitle,
|
|
episode: currentEpisode,
|
|
source: currentSource,
|
|
};
|
|
|
|
console.log('[PlaySync] Broadcasting play:change:', state);
|
|
watchRoom.changeVideo(state);
|
|
|
|
// 更新跟踪值
|
|
lastBroadcastRef.current = currentState;
|
|
}, 500); // 减少延迟到500ms
|
|
|
|
return () => clearTimeout(timer);
|
|
}, [isOwner, socket, currentRoom, isInRoom, watchRoom, videoId, currentEpisode, currentSource, videoUrl, videoName, videoYear, searchTitle, artPlayerRef]);
|
|
|
|
// 房主:加入房间时立即广播当前播放状态
|
|
const lastRoomStateRef = useRef<{ isOwner: boolean; roomId: string | null }>({ isOwner: false, roomId: null });
|
|
|
|
useEffect(() => {
|
|
const currentRoomId = currentRoom?.id || null;
|
|
const prevRoomState = lastRoomStateRef.current;
|
|
|
|
// 检测是否刚成为房主或刚加入房间
|
|
const justBecameOwner = !prevRoomState.isOwner && isOwner;
|
|
const justJoinedRoom = !prevRoomState.roomId && currentRoomId;
|
|
|
|
// 更新ref
|
|
lastRoomStateRef.current = { isOwner, roomId: currentRoomId };
|
|
|
|
if (!isOwner || !socket || !currentRoom || !isInRoom || !watchRoom) return;
|
|
if (!videoId || !videoUrl) return;
|
|
if (!justBecameOwner && !justJoinedRoom) return;
|
|
|
|
console.log('[PlaySync] Owner joined room, broadcasting current state immediately:', {
|
|
justBecameOwner,
|
|
justJoinedRoom
|
|
});
|
|
|
|
// 立即广播当前状态
|
|
const state: PlayState = {
|
|
type: 'play',
|
|
url: videoUrl,
|
|
currentTime: artPlayerRef.current?.currentTime || 0,
|
|
isPlaying: artPlayerRef.current?.playing || false,
|
|
videoId,
|
|
videoName,
|
|
videoYear,
|
|
searchTitle,
|
|
episode: currentEpisode,
|
|
source: currentSource,
|
|
};
|
|
|
|
// 短暂延迟确保房间连接已稳定
|
|
const timer = setTimeout(() => {
|
|
console.log('[PlaySync] Broadcasting play:change on room join:', state);
|
|
watchRoom.changeVideo(state);
|
|
|
|
// 同时更新跟踪值,避免立即重复广播
|
|
lastBroadcastRef.current = {
|
|
videoId,
|
|
source: currentSource,
|
|
episode: currentEpisode || 1,
|
|
};
|
|
}, 300);
|
|
|
|
return () => clearTimeout(timer);
|
|
}, [isOwner, currentRoom, socket, isInRoom, watchRoom, videoId, videoUrl, videoName, videoYear, searchTitle, currentEpisode, currentSource, artPlayerRef]);
|
|
|
|
return {
|
|
isInRoom,
|
|
isOwner,
|
|
shouldDisableControls: isInRoom && !isOwner, // 房员禁用某些控制
|
|
broadcastPlayState, // 导出供手动调用
|
|
};
|
|
}
|