|
|
@@ -18,6 +18,12 @@ export const WelcomePage = () => {
|
|
|
const playerRef = useRef<VideoJsPlayer | null>(null);
|
|
|
const [viewState, setViewState] = useState<WelcomeViewState>('welcome');
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
+ const isVideoReadyRef = useRef(false); // 视频是否已加载完成
|
|
|
+ const loadingStartTimeRef = useRef<number | null>(null); // 加载开始时间
|
|
|
+ const userInteractionRef = useRef(false); // 用户是否已交互(用于确保声音播放)
|
|
|
+ const viewStateRef = useRef<WelcomeViewState>('welcome'); // 用于在回调中访问最新状态
|
|
|
+ const checkIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null); // 检查定时器
|
|
|
+ const fallbackTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); // 兜底定时器
|
|
|
|
|
|
useEffect(() => {
|
|
|
// 记录用户当前所在页面
|
|
|
@@ -26,6 +32,16 @@ export const WelcomePage = () => {
|
|
|
// 记录路由访问
|
|
|
markRouteVisited('/welcome');
|
|
|
|
|
|
+ // 🚀 提前预加载视频资源(使用 link preload)
|
|
|
+ const videoUrl = import.meta.env.VITE_WELCOME_VIDEO_URL || 'https://tolan.oss-cn-hangzhou.aliyuncs.com/ABC.mp4';
|
|
|
+ const preloadLink = document.createElement('link');
|
|
|
+ preloadLink.rel = 'preload';
|
|
|
+ preloadLink.as = 'video';
|
|
|
+ preloadLink.href = videoUrl;
|
|
|
+ preloadLink.crossOrigin = 'anonymous';
|
|
|
+ document.head.appendChild(preloadLink);
|
|
|
+ console.log('[WelcomePage] 已添加视频预加载链接:', videoUrl);
|
|
|
+
|
|
|
// 移动端视口高度修复:处理浏览器地址栏/工具栏导致的视口高度变化
|
|
|
const setViewportHeight = () => {
|
|
|
// 获取实际可视区域高度(不包括浏览器UI)
|
|
|
@@ -44,6 +60,10 @@ export const WelcomePage = () => {
|
|
|
return () => {
|
|
|
window.removeEventListener('resize', setViewportHeight);
|
|
|
window.removeEventListener('orientationchange', setViewportHeight);
|
|
|
+ // 清理预加载链接
|
|
|
+ if (preloadLink.parentNode) {
|
|
|
+ preloadLink.parentNode.removeChild(preloadLink);
|
|
|
+ }
|
|
|
};
|
|
|
}, []);
|
|
|
|
|
|
@@ -88,6 +108,57 @@ export const WelcomePage = () => {
|
|
|
videoElement.style.opacity = '0';
|
|
|
videoElement.style.position = 'absolute';
|
|
|
videoElement.style.top = '-9999px';
|
|
|
+
|
|
|
+ // 🚀 强制开始加载视频(立即开始预加载)
|
|
|
+ try {
|
|
|
+ videoElement.load(); // 强制开始加载
|
|
|
+ console.log('[WelcomePage] 已强制开始加载视频');
|
|
|
+ } catch (error) {
|
|
|
+ console.warn('[WelcomePage] 强制加载失败(可能已开始加载):', error);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 监听视频加载状态
|
|
|
+ const handleCanPlayThrough = () => {
|
|
|
+ console.log('[WelcomePage] 视频已加载完成,可以播放');
|
|
|
+ isVideoReadyRef.current = true;
|
|
|
+ // 如果正在加载状态,检查是否可以开始播放
|
|
|
+ if (viewStateRef.current === 'loading' && loadingStartTimeRef.current) {
|
|
|
+ checkAndStartPlayback();
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleError = (error: Event) => {
|
|
|
+ console.error('[WelcomePage] 视频加载错误:', error);
|
|
|
+ const videoEl = error.target as HTMLVideoElement;
|
|
|
+ if (videoEl && videoEl.error) {
|
|
|
+ console.error('[WelcomePage] 视频错误详情:', {
|
|
|
+ code: videoEl.error?.code,
|
|
|
+ message: videoEl.error?.message,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ // 即使出错也尝试继续(可能是网络问题,稍后重试)
|
|
|
+ isVideoReadyRef.current = false;
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleLoadedData = () => {
|
|
|
+ console.log('[WelcomePage] 视频数据已加载');
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleLoadedMetadata = () => {
|
|
|
+ console.log('[WelcomePage] 视频元数据已加载');
|
|
|
+ };
|
|
|
+
|
|
|
+ // 绑定事件
|
|
|
+ videoElement.addEventListener('canplaythrough', handleCanPlayThrough);
|
|
|
+ videoElement.addEventListener('error', handleError);
|
|
|
+ videoElement.addEventListener('loadeddata', handleLoadedData);
|
|
|
+ videoElement.addEventListener('loadedmetadata', handleLoadedMetadata);
|
|
|
+
|
|
|
+ // 保存事件处理器以便清理
|
|
|
+ (player as any)._canPlayThroughHandler = handleCanPlayThrough;
|
|
|
+ (player as any)._errorHandler = handleError;
|
|
|
+ (player as any)._loadedDataHandler = handleLoadedData;
|
|
|
+ (player as any)._loadedMetadataHandler = handleLoadedMetadata;
|
|
|
}
|
|
|
|
|
|
// 持续监控并移除 controls 属性
|
|
|
@@ -126,10 +197,119 @@ export const WelcomePage = () => {
|
|
|
if (endedHandler) {
|
|
|
player.off('ended', endedHandler);
|
|
|
}
|
|
|
+ // 清理视频元素事件监听器
|
|
|
+ const videoElement = player.el()?.querySelector('video') as HTMLVideoElement;
|
|
|
+ if (videoElement) {
|
|
|
+ const canPlayThroughHandler = (player as any)._canPlayThroughHandler;
|
|
|
+ const errorHandler = (player as any)._errorHandler;
|
|
|
+ const loadedDataHandler = (player as any)._loadedDataHandler;
|
|
|
+ const loadedMetadataHandler = (player as any)._loadedMetadataHandler;
|
|
|
+ const videoEndedHandler = (player as any)._videoEndedHandler;
|
|
|
+ if (canPlayThroughHandler) {
|
|
|
+ videoElement.removeEventListener('canplaythrough', canPlayThroughHandler);
|
|
|
+ }
|
|
|
+ if (errorHandler) {
|
|
|
+ videoElement.removeEventListener('error', errorHandler);
|
|
|
+ }
|
|
|
+ if (loadedDataHandler) {
|
|
|
+ videoElement.removeEventListener('loadeddata', loadedDataHandler);
|
|
|
+ }
|
|
|
+ if (loadedMetadataHandler) {
|
|
|
+ videoElement.removeEventListener('loadedmetadata', loadedMetadataHandler);
|
|
|
+ }
|
|
|
+ if (videoEndedHandler) {
|
|
|
+ videoElement.removeEventListener('ended', videoEndedHandler);
|
|
|
+ }
|
|
|
+ }
|
|
|
// 注意:不在这里销毁播放器,因为视频需要保持加载状态
|
|
|
}
|
|
|
};
|
|
|
- }, [navigate]);
|
|
|
+ }, [navigate, viewState]);
|
|
|
+
|
|
|
+ // 更新 viewState 时同步更新 ref
|
|
|
+ useEffect(() => {
|
|
|
+ viewStateRef.current = viewState;
|
|
|
+ }, [viewState]);
|
|
|
+
|
|
|
+ // 组件卸载时清理定时器
|
|
|
+ useEffect(() => {
|
|
|
+ return () => {
|
|
|
+ if (checkIntervalRef.current) {
|
|
|
+ clearInterval(checkIntervalRef.current);
|
|
|
+ checkIntervalRef.current = null;
|
|
|
+ }
|
|
|
+ if (fallbackTimerRef.current) {
|
|
|
+ clearTimeout(fallbackTimerRef.current);
|
|
|
+ fallbackTimerRef.current = null;
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ // 检查并开始播放(结合加载状态和最小缓冲时间)
|
|
|
+ const checkAndStartPlayback = () => {
|
|
|
+ if (!playerRef.current || viewStateRef.current !== 'loading') {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const now = Date.now();
|
|
|
+ const minLoadingTime = 3000; // 最小加载时间3秒
|
|
|
+ const elapsed = loadingStartTimeRef.current
|
|
|
+ ? now - loadingStartTimeRef.current
|
|
|
+ : 0;
|
|
|
+ const remainingTime = Math.max(0, minLoadingTime - elapsed);
|
|
|
+
|
|
|
+ // 如果视频已准备好且达到最小加载时间,开始播放
|
|
|
+ if (isVideoReadyRef.current && elapsed >= minLoadingTime) {
|
|
|
+ console.log('[WelcomePage] 视频已准备好,开始播放');
|
|
|
+ // 清理定时器
|
|
|
+ if (checkIntervalRef.current) {
|
|
|
+ clearInterval(checkIntervalRef.current);
|
|
|
+ checkIntervalRef.current = null;
|
|
|
+ }
|
|
|
+ if (fallbackTimerRef.current) {
|
|
|
+ clearTimeout(fallbackTimerRef.current);
|
|
|
+ fallbackTimerRef.current = null;
|
|
|
+ }
|
|
|
+ setIsLoading(false);
|
|
|
+ setViewState('playing');
|
|
|
+ } else if (isVideoReadyRef.current && remainingTime > 0) {
|
|
|
+ // 视频已准备好,但还需要等待最小加载时间
|
|
|
+ console.log(`[WelcomePage] 视频已准备好,等待 ${remainingTime}ms 后播放`);
|
|
|
+ // 清理检查定时器
|
|
|
+ if (checkIntervalRef.current) {
|
|
|
+ clearInterval(checkIntervalRef.current);
|
|
|
+ checkIntervalRef.current = null;
|
|
|
+ }
|
|
|
+ setTimeout(() => {
|
|
|
+ if (viewStateRef.current === 'loading') {
|
|
|
+ setIsLoading(false);
|
|
|
+ setViewState('playing');
|
|
|
+ }
|
|
|
+ }, remainingTime);
|
|
|
+ } else if (!isVideoReadyRef.current && elapsed >= minLoadingTime) {
|
|
|
+ // 达到最小加载时间但视频未准备好,再等待一段时间
|
|
|
+ console.log('[WelcomePage] 达到最小加载时间,但视频未完全准备好,继续等待...');
|
|
|
+ // 清理检查定时器
|
|
|
+ if (checkIntervalRef.current) {
|
|
|
+ clearInterval(checkIntervalRef.current);
|
|
|
+ checkIntervalRef.current = null;
|
|
|
+ }
|
|
|
+ const waitTimer = setTimeout(() => {
|
|
|
+ if (viewStateRef.current === 'loading') {
|
|
|
+ if (isVideoReadyRef.current) {
|
|
|
+ setIsLoading(false);
|
|
|
+ setViewState('playing');
|
|
|
+ } else {
|
|
|
+ // 即使未完全准备好也尝试播放(可能是网络慢)
|
|
|
+ console.warn('[WelcomePage] 视频未完全准备好,尝试播放');
|
|
|
+ setIsLoading(false);
|
|
|
+ setViewState('playing');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }, 2000); // 额外等待2秒
|
|
|
+ (playerRef.current as any)._waitTimer = waitTimer;
|
|
|
+ }
|
|
|
+ };
|
|
|
|
|
|
// 当进入播放状态时,显示视频并开始播放
|
|
|
useEffect(() => {
|
|
|
@@ -152,29 +332,79 @@ export const WelcomePage = () => {
|
|
|
videoElement.style.height = '100%';
|
|
|
videoElement.style.zIndex = '2';
|
|
|
|
|
|
- // 确保视频未静音
|
|
|
+ // 绑定视频播放结束事件(原生事件,确保可靠)
|
|
|
+ const handleVideoEnded = () => {
|
|
|
+ console.log('[WelcomePage] 视频播放完成,跳转到 /naming');
|
|
|
+ navigate('/naming');
|
|
|
+ };
|
|
|
+ videoElement.addEventListener('ended', handleVideoEnded);
|
|
|
+ // 保存处理器以便清理
|
|
|
+ (player as any)._videoEndedHandler = handleVideoEnded;
|
|
|
+
|
|
|
+ // 确保视频未静音(用户已交互,应该可以播放声音)
|
|
|
player.muted(false);
|
|
|
videoElement.muted = false;
|
|
|
videoElement.volume = 1.0;
|
|
|
|
|
|
- // 尝试播放
|
|
|
+ // 由于用户已点击(交互),移动端Chrome应该允许播放声音
|
|
|
+ // 尝试播放(有声)
|
|
|
const playPromise = player.play();
|
|
|
if (playPromise) {
|
|
|
playPromise
|
|
|
+ .then(() => {
|
|
|
+ console.log('[WelcomePage] 视频播放成功(有声)');
|
|
|
+ // 确保未静音
|
|
|
+ player.muted(false);
|
|
|
+ videoElement.muted = false;
|
|
|
+ videoElement.volume = 1.0;
|
|
|
+ })
|
|
|
.catch((error: unknown) => {
|
|
|
- console.warn('[WelcomePage] 自动播放失败,尝试静音播放:', error);
|
|
|
+ console.warn('[WelcomePage] 有声播放失败,尝试静音播放:', error);
|
|
|
+ // 如果有声播放失败,尝试静音播放
|
|
|
player.muted(true);
|
|
|
+ videoElement.muted = true;
|
|
|
const mutedPlayPromise = player.play();
|
|
|
if (mutedPlayPromise) {
|
|
|
- mutedPlayPromise.catch((err: unknown) => {
|
|
|
- console.error('[WelcomePage] 静音播放也失败:', err);
|
|
|
- });
|
|
|
+ mutedPlayPromise
|
|
|
+ .then(() => {
|
|
|
+ console.log('[WelcomePage] 静音播放成功,尝试恢复声音');
|
|
|
+ // 静音播放成功后,尝试恢复声音(需要用户再次交互)
|
|
|
+ // 但由于用户已点击,应该可以恢复
|
|
|
+ setTimeout(() => {
|
|
|
+ try {
|
|
|
+ player.muted(false);
|
|
|
+ videoElement.muted = false;
|
|
|
+ videoElement.volume = 1.0;
|
|
|
+ console.log('[WelcomePage] 已尝试恢复声音');
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('[WelcomePage] 恢复声音失败:', e);
|
|
|
+ }
|
|
|
+ }, 100);
|
|
|
+ })
|
|
|
+ .catch((err: unknown) => {
|
|
|
+ console.error('[WelcomePage] 静音播放也失败:', err);
|
|
|
+ });
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ // 清理函数:移除事件监听器
|
|
|
+ return () => {
|
|
|
+ if (playerRef.current) {
|
|
|
+ const player = playerRef.current;
|
|
|
+ const videoElement = player.el()?.querySelector('video') as HTMLVideoElement;
|
|
|
+ if (videoElement) {
|
|
|
+ const videoEndedHandler = (player as any)._videoEndedHandler;
|
|
|
+ if (videoEndedHandler) {
|
|
|
+ videoElement.removeEventListener('ended', videoEndedHandler);
|
|
|
+ (player as any)._videoEndedHandler = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
}
|
|
|
- }, [viewState]);
|
|
|
+ }, [viewState, navigate]);
|
|
|
|
|
|
// 监听播放结束事件(备用方案,确保事件已绑定)
|
|
|
useEffect(() => {
|
|
|
@@ -193,16 +423,90 @@ export const WelcomePage = () => {
|
|
|
}
|
|
|
}, [navigate, viewState]); // 当播放状态改变时,确保事件已绑定
|
|
|
|
|
|
- const handleHelloClick = () => {
|
|
|
+ const handleHelloClick = async () => {
|
|
|
+ // 标记用户已交互(用于确保声音播放)
|
|
|
+ userInteractionRef.current = true;
|
|
|
+
|
|
|
+ // 清理之前的定时器
|
|
|
+ if (checkIntervalRef.current) {
|
|
|
+ clearInterval(checkIntervalRef.current);
|
|
|
+ checkIntervalRef.current = null;
|
|
|
+ }
|
|
|
+ if (fallbackTimerRef.current) {
|
|
|
+ clearTimeout(fallbackTimerRef.current);
|
|
|
+ fallbackTimerRef.current = null;
|
|
|
+ }
|
|
|
+
|
|
|
// 切换到加载状态
|
|
|
setViewState('loading');
|
|
|
setIsLoading(true);
|
|
|
+ loadingStartTimeRef.current = Date.now();
|
|
|
|
|
|
- // 3秒加载倒计时
|
|
|
- setTimeout(() => {
|
|
|
- setIsLoading(false);
|
|
|
- setViewState('playing');
|
|
|
- }, 3000);
|
|
|
+ // 🔑 关键修复:在用户交互的直接调用链中立即尝试播放
|
|
|
+ // 这样可以"锁定"用户交互权限,即使视频还没准备好
|
|
|
+ if (playerRef.current) {
|
|
|
+ const player = playerRef.current;
|
|
|
+ const videoElement = player.el()?.querySelector('video') as HTMLVideoElement;
|
|
|
+
|
|
|
+ if (videoElement) {
|
|
|
+ // 确保未静音
|
|
|
+ player.muted(false);
|
|
|
+ videoElement.muted = false;
|
|
|
+ videoElement.volume = 1.0;
|
|
|
+
|
|
|
+ // 立即尝试播放(在用户交互的直接调用链中)
|
|
|
+ // 如果视频还没准备好,这会失败,但没关系,我们会在视频准备好后再播放
|
|
|
+ try {
|
|
|
+ const playPromise = player.play();
|
|
|
+ if (playPromise) {
|
|
|
+ await playPromise;
|
|
|
+ console.log('[WelcomePage] ✅ 在用户交互中立即播放成功');
|
|
|
+ // 如果立即播放成功,直接进入播放状态
|
|
|
+ setIsLoading(false);
|
|
|
+ setViewState('playing');
|
|
|
+ return; // 成功播放,直接返回
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.log('[WelcomePage] 视频未准备好,等待加载完成后再播放:', error);
|
|
|
+ // 播放失败是预期的(视频可能还没准备好),继续等待加载
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果立即播放失败(视频未准备好),使用原来的逻辑等待加载
|
|
|
+ // 检查视频是否已准备好
|
|
|
+ if (isVideoReadyRef.current) {
|
|
|
+ // 视频已准备好,使用最小加载时间
|
|
|
+ setTimeout(() => {
|
|
|
+ checkAndStartPlayback();
|
|
|
+ }, 0);
|
|
|
+ } else {
|
|
|
+ // 视频未准备好,等待加载完成或达到最小时间
|
|
|
+ // 设置一个兜底定时器(最多等待5秒)
|
|
|
+ fallbackTimerRef.current = setTimeout(() => {
|
|
|
+ if (viewStateRef.current === 'loading') {
|
|
|
+ console.warn('[WelcomePage] 加载超时,强制开始播放');
|
|
|
+ if (checkIntervalRef.current) {
|
|
|
+ clearInterval(checkIntervalRef.current);
|
|
|
+ checkIntervalRef.current = null;
|
|
|
+ }
|
|
|
+ setIsLoading(false);
|
|
|
+ setViewState('playing');
|
|
|
+ }
|
|
|
+ }, 5000);
|
|
|
+
|
|
|
+ // 定期检查是否可以开始播放
|
|
|
+ checkIntervalRef.current = setInterval(() => {
|
|
|
+ if (viewStateRef.current === 'loading') {
|
|
|
+ checkAndStartPlayback();
|
|
|
+ } else {
|
|
|
+ if (checkIntervalRef.current) {
|
|
|
+ clearInterval(checkIntervalRef.current);
|
|
|
+ checkIntervalRef.current = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }, 100);
|
|
|
+ }
|
|
|
};
|
|
|
|
|
|
return (
|
|
|
@@ -259,8 +563,12 @@ export const WelcomePage = () => {
|
|
|
data-setup="{}"
|
|
|
controlsList="nodownload nofullscreen noremoteplayback"
|
|
|
disablePictureInPicture
|
|
|
+ preload="auto"
|
|
|
>
|
|
|
- <source src="https://tolan.oss-cn-hangzhou.aliyuncs.com/ABC.mp4" type="video/mp4" />
|
|
|
+ <source
|
|
|
+ src={import.meta.env.VITE_WELCOME_VIDEO_URL || 'https://tolan.oss-cn-hangzhou.aliyuncs.com/ABC.mp4'}
|
|
|
+ type="video/mp4"
|
|
|
+ />
|
|
|
<p className="vjs-no-js">
|
|
|
您的浏览器不支持HTML5视频,请升级浏览器。
|
|
|
</p>
|