React实现滚动控制视频播放,播放完成后可滚动至后续内容
React实现滚动控制的固定视窗视频
需求
- 用户滚动页面时,视频随滚动进度播放;回滚页面时,视频回退到对应进度
- 视频播放过程中始终完整显示在视窗内,不会随滚动移出视野
- 视频播放结束后,允许用户继续滚动到页面其他内容
遇到的问题
试过Scrollyvideo这类第三方库,但达不到预期效果。比如React Scrollyvideo无法让视频固定在视窗,滚动时视频会移出视野,即使默认设置了position: sticky也无法解决。
现有半实现代码的问题
- 从Codepen示例转换到React的版本,核心逻辑可用,但视频下方无法显示其他内容:
import React, { useEffect, useRef } from "react"; const ScrollVideo = () => { const videoRef = useRef(null); const playbackConst = 500; // 每滚动500px对应视频播放1秒 const videoLength = 10; // 视频总时长(秒) const videoHeight = `${videoLength * playbackConst}px`; // 占位容器高度 useEffect(() => { const vid = videoRef.current; const scrollPlay = () => { const frameNumber = window.scrollY / playbackConst; // 限制播放进度在视频时长范围内 vid.currentTime = Math.min(frameNumber, videoLength); window.requestAnimationFrame(scrollPlay); }; window.requestAnimationFrame(scrollPlay); }, []); return ( <> <div id="video-placeholder" style={{ height: videoHeight }}> <video ref={videoRef} tabIndex={0} preload="auto" style={{ position: "fixed", top: "50%", left: "50%", transform: "translate(-50%, -50%)", maxWidth: "100%", maxHeight: "100%", }} > <source type='video/mp4; codecs="avc1.42E01E, mp4a.40.2"' src="/neural.mp4" ></source> </video> </div> {/* 这里可以添加视频下方的其他内容 */} <div style={{ height: "1000px", backgroundColor: "#f0f0f0" }}> <h2>视频播放完成后可滚动到这里</h2> <p>这是页面的其他内容区域</p> </div> </> ); }; export default ScrollVideo;
问题:原代码未预留后续内容位置,视频定位逻辑无法保证完整适配视窗;优化后改用transform: translate实现居中,限制最大宽高确保视频完整显示。
- 另一版本实现了核心功能,但依赖不推荐的滚动劫持(
e.preventDefault()),且未做视窗触发条件判断:
import React, { useState, useRef, useEffect } from "react"; import ReactPlayer from "react-player"; const ScrollVideo = () => { const [duration, setDuration] = useState(0); const [isVideoInView, setIsVideoInView] = useState(false); const playerRef = useRef(null); const videoSectionRef = useRef(null); // 监听视频区域是否在视窗内 useEffect(() => { const observer = new IntersectionObserver( ([entry]) => { setIsVideoInView(entry.isIntersecting); }, { threshold: 0.9 } // 当视频区域90%进入视窗时触发 ); if (videoSectionRef.current) { observer.observe(videoSectionRef.current); } return () => observer.disconnect(); }, []); const handleScroll = (e) => { if (!isVideoInView || !playerRef.current || !duration) return; const currentTime = playerRef.current.getCurrentTime(); let newTime = currentTime + e.deltaY * 0.02; if (newTime <= 0) { playerRef.current.seekTo(0, "seconds"); } else if (newTime >= duration) { playerRef.current.seekTo(duration, "seconds"); // 视频播放完毕后,不再阻止滚动 return; } else { // 仅在视频未播放完毕且在视窗内时,才控制滚动 e.preventDefault(); playerRef.current.seekTo(newTime, "seconds"); } }; useEffect(() => { window.addEventListener("wheel", handleScroll, { passive: false }); return () => { window.removeEventListener("wheel", handleScroll); }; }, [duration, isVideoInView]); return ( <> <div ref={videoSectionRef} style={{ width: "100%", height: "100vh", display: "flex", alignItems: "center", justifyContent: "center" }} > <ReactPlayer ref={playerRef} url="neural.mp4" width="80%" height="80%" onDuration={setDuration} controls={false} /> </div> <div style={{ height: "1000px", backgroundColor: "#f0f0f0" }}> <h2>视频播放完成后可滚动到这里</h2> <p>这是页面的其他内容区域</p> </div> </> ); }; export default ScrollVideo;
优化点:添加IntersectionObserver监听视频区域的视窗可见性,仅在视频进入视窗时触发滚动控制;视频播放完毕后取消滚动劫持,允许用户继续浏览后续内容。
推荐方案(无滚动劫持,适配视窗)
结合两种方案的优点,采用占位容器+fixed视频的方式,避免滚动劫持,同时确保视频始终固定在视窗内,且支持后续内容滚动:
import React, { useEffect, useRef, useState } from "react"; const ScrollControlledVideo = () => { const videoRef = useRef(null); const placeholderRef = useRef(null); const [videoDuration, setVideoDuration] = useState(0); const playbackRate = 500; // 每滚动500px对应1秒视频 // 获取视频实际时长并设置占位容器高度 useEffect(() => { const vid = videoRef.current; const handleLoadedMetadata = () => { setVideoDuration(vid.duration); if (placeholderRef.current) { placeholderRef.current.style.height = `${vid.duration * playbackRate}px`; } }; vid.addEventListener("loadedmetadata", handleLoadedMetadata); return () => vid.removeEventListener("loadedmetadata", handleLoadedMetadata); }, []); // 监听滚动控制视频播放进度 useEffect(() => { if (!videoDuration) return; const handleScroll = () => { const scrollTop = window.scrollY; const maxScroll = videoDuration * playbackRate; // 计算当前播放进度,限制在0到视频时长范围内 const currentTime = Math.min(scrollTop / playbackRate, videoDuration); if (videoRef.current) { videoRef.current.currentTime = currentTime; } }; window.addEventListener("scroll", handleScroll, { passive: true }); return () => window.removeEventListener("scroll", handleScroll); }, [videoDuration]); return ( <div className="scroll-video-container"> {/* 占位容器:提供滚动空间,高度对应视频完整播放所需的滚动距离 */} <div ref={placeholderRef} className="video-placeholder"></div> {/* 固定在视窗的视频 */} <video ref={videoRef} preload="auto" muted // 浏览器自动播放要求静音 playsInline style={{ position: "fixed", top: "50%", left: "50%", transform: "translate(-50%, -50%)", maxWidth: "95vw", maxHeight: "95vh", objectFit: "contain", }} > <source src="/neural.mp4" type="video/mp4" /> 您的浏览器不支持视频播放 </video> {/* 视频下方的其他内容 */} <section className="post-video-content"> <h2>视频播放完成,继续浏览</h2> <div style={{ height: "800px", padding: "2rem", backgroundColor: "#f8f9fa" }}> <p>这里是页面的后续内容区域,当滚动到视频占位容器底部后,即可看到此部分。</p> </div> </section> </div> ); }; export default ScrollControlledVideo;
方案说明
- 占位容器:根据视频实际时长动态计算高度,提供足够的滚动空间,用户滚动此容器的全程对应视频的完整播放周期。
- 视频固定定位:使用
fixed+transform实现居中显示,通过maxWidth/maxHeight确保视频始终完整适配视窗,不会超出边界。 - 滚动监听:采用
passive: true的滚动监听,完全符合浏览器性能规范,无滚动劫持问题,不会阻塞页面滚动。 - 动态适配:通过
loadedmetadata事件获取视频实际时长,自动调整占位容器高度,适配不同时长的视频文件。
内容的提问来源于stack exchange,提问作者ktt
相关产品推荐
相关产品推荐

