| | |
| | | /** 派单成功转场视频 */ |
| | | export const DISPATCH_VIDEO_SRC = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/video/vehicle_approaching_v1_file_w_1128@h_1268.mp4'; |
| | | export const DISPATCH_VIDEO_POSTER = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/video/vehicle_approaching_poster_v1_file_w_1128@h_1268.jpg'; |
| | | /** 距视频结束多久提前进入 settle 归位阶段(秒) */ |
| | | export const DISPATCH_SETTLE_LEAD_SECONDS = 0.8; |
| | | /** 兜底超时(ms):防卡死安全阀——视频加载/播放失败时 onTimeUpdate/onEnded 都不触发,超过此时长强制归位 */ |
| | | export const DISPATCH_FALLBACK_MS = 10000; |
| | | |
| | |
| | | const fallbackTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); |
| | | const settleTriggeredRef = useRef(false); |
| | | |
| | | // 背景缩小精确计时:微信 video 的 timeupdate 间隔约 265ms,无法直接捕获目标时刻。 |
| | | // 方案:timeupdate 记录 {播放时间, 墙上时钟} 作同步锚点,自适应 setTimeout 在两次事件间 |
| | | // 通过墙上时钟插值估算播放进度,提前唤醒、递归逼近,最终触发精度约 4ms。 |
| | | const lastTimeSyncRef = useRef({ currentTime: 0, wallClock: 0 }); |
| | | const videoDurationRef = useRef(0); |
| | | const shrinkTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); |
| | | const shrinkTriggeredRef = useRef(false); |
| | | |
| | | /** 背景缩小提前量(秒):在视频结束前触发 settle。视频全长 3.333s,即 currentTime ≈ 2.278s 处。 */ |
| | | const BG_SHRINK_LEAD_SEC = 1.055; |
| | | |
| | | // 进入归位落定阶段(幂等,只生效一次) |
| | | const handleDispatchVideoEnded = () => { |
| | | const enterSettle = () => { |
| | | if (settleTriggeredRef.current) return; |
| | | settleTriggeredRef.current = true; |
| | | if (fallbackTimerRef.current) clearTimeout(fallbackTimerRef.current); |
| | |
| | | }, 600); |
| | | }; |
| | | |
| | | // 播放进度更新:距视频结束还剩 DISPATCH_SETTLE_LEAD_SECONDS 秒时提前进入 settle |
| | | /** |
| | | * 自适应缩小定时器。每次 timeupdate 或定时器到期时调用,计算距目标还有多久: |
| | | * - 已到达 → 调用 enterSettle 进入 settle |
| | | * - 未到达 → setTimeout 提前 10ms 唤醒,到期后用墙上时钟插值修正、递归检查 |
| | | */ |
| | | const scheduleShrinkCheck = (currentTime: number) => { |
| | | if (shrinkTriggeredRef.current) return; |
| | | |
| | | const targetSec = videoDurationRef.current - BG_SHRINK_LEAD_SEC; |
| | | const remaining = targetSec - currentTime; |
| | | |
| | | if (remaining <= 0) { |
| | | shrinkTriggeredRef.current = true; |
| | | if (shrinkTimerRef.current) clearTimeout(shrinkTimerRef.current); |
| | | enterSettle(); |
| | | return; |
| | | } |
| | | |
| | | if (shrinkTimerRef.current) clearTimeout(shrinkTimerRef.current); |
| | | shrinkTimerRef.current = setTimeout(() => { |
| | | const { currentTime: lastCT, wallClock: lastWC } = lastTimeSyncRef.current; |
| | | if (lastWC === 0) return; |
| | | const estimated = lastCT + (Date.now() - lastWC) / 1000; |
| | | scheduleShrinkCheck(estimated); |
| | | }, Math.max(remaining * 1000 - 10,30)); |
| | | }; |
| | | |
| | | const handleDispatchVideoTimeUpdate = (e: any) => { |
| | | const { currentTime, duration } = e?.detail || {}; |
| | | if (typeof currentTime !== 'number' || typeof duration !== 'number' || duration <= 0) return; |
| | | if (duration - currentTime <= DISPATCH_SETTLE_LEAD_SECONDS) { |
| | | handleDispatchVideoEnded(); |
| | | } |
| | | |
| | | lastTimeSyncRef.current = { currentTime, wallClock: Date.now() }; |
| | | videoDurationRef.current = duration; |
| | | |
| | | scheduleShrinkCheck(currentTime); |
| | | }; |
| | | |
| | | useEffect(() => { |
| | |
| | | if (settleTriggeredRef.current) return; |
| | | setDispatchPhase('video'); |
| | | }, 32); |
| | | fallbackTimerRef.current = setTimeout(() => handleDispatchVideoEnded(), DISPATCH_FALLBACK_MS); |
| | | fallbackTimerRef.current = setTimeout(() => enterSettle(), DISPATCH_FALLBACK_MS); |
| | | } |
| | | return () => { |
| | | if (enterTimerRef.current) clearTimeout(enterTimerRef.current); |
| | | if (settleTimerRef.current) clearTimeout(settleTimerRef.current); |
| | | if (fallbackTimerRef.current) clearTimeout(fallbackTimerRef.current); |
| | | if (shrinkTimerRef.current) clearTimeout(shrinkTimerRef.current); |
| | | }; |
| | | }, []); |
| | | |
| | |
| | | dispatchPhase, |
| | | transitionDone, |
| | | shouldPlayEnter, |
| | | enterSettle, |
| | | handleDispatchVideoTimeUpdate, |
| | | handleDispatchVideoEnded, |
| | | }; |
| | | } |