/* Variant B — «Кинохроника» Видеофон Читы + цветовая засветка маршрута, фото-узлы. Точки распределены по полному внешнему кругу. Данные — window.ORBIT_ROUTES (из БД), подписи — window.ORBIT_I18N. */ // Seamless loop with ONE active video at a time. // We snapshot the last frame onto a canvas right before the seek-to-0, // then crossfade the canvas out as the video resumes from frame 0. // Only one decoder is ever running → far cheaper than dual-video crossfade. const SeamlessVideo = ({ src, poster, className }) => { const { useEffect, useRef } = React; const videoRef = useRef(null); const canvasRef = useRef(null); const fade = 0.7; useEffect(() => { const v = videoRef.current; const c = canvasRef.current; if (!v || !c) return; let handing = false; let timer = null; const onTime = () => { const dur = v.duration; if (!isFinite(dur) || dur <= 0 || handing) return; if (dur - v.currentTime <= 0.25) { handing = true; // 1. Snapshot the current frame. const w = v.videoWidth || 1280; const h = v.videoHeight || 720; if (c.width !== w) c.width = w; if (c.height !== h) c.height = h; const ctx = c.getContext("2d"); try { ctx.drawImage(v, 0, 0, w, h); } catch (_) {} // 2. Show the frozen frame instantly (cover the seek gap). c.style.transition = "none"; c.style.opacity = "1"; // 3. Seek + resume. try { v.currentTime = 0; } catch (_) {} v.play().catch(() => {}); // 4. After the video reports a new frame, fade the canvas out. const startFade = () => { requestAnimationFrame(() => { c.style.transition = `opacity ${fade}s linear`; c.style.opacity = "0"; }); }; if ("requestVideoFrameCallback" in v) { v.requestVideoFrameCallback(startFade); } else { v.addEventListener("seeked", startFade, { once: true }); } timer = setTimeout(() => { handing = false; }, fade * 1000 + 200); } }; v.addEventListener("timeupdate", onTime); return () => { v.removeEventListener("timeupdate", onTime); if (timer) clearTimeout(timer); }; }, []); return ( <>