You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React实现滚动控制视频播放,播放完成后可滚动至后续内容

React实现滚动控制的固定视窗视频

需求

  • 用户滚动页面时,视频随滚动进度播放;回滚页面时,视频回退到对应进度
  • 视频播放过程中始终完整显示在视窗内,不会随滚动移出视野
  • 视频播放结束后,允许用户继续滚动到页面其他内容

遇到的问题

试过Scrollyvideo这类第三方库,但达不到预期效果。比如React Scrollyvideo无法让视频固定在视窗,滚动时视频会移出视野,即使默认设置了position: sticky也无法解决。

现有半实现代码的问题

  1. 从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实现居中,限制最大宽高确保视频完整显示。

  1. 另一版本实现了核心功能,但依赖不推荐的滚动劫持(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;

方案说明

  1. 占位容器:根据视频实际时长动态计算高度,提供足够的滚动空间,用户滚动此容器的全程对应视频的完整播放周期。
  2. 视频固定定位:使用fixed+transform实现居中显示,通过maxWidth/maxHeight确保视频始终完整适配视窗,不会超出边界。
  3. 滚动监听:采用passive: true的滚动监听,完全符合浏览器性能规范,无滚动劫持问题,不会阻塞页面滚动。
  4. 动态适配:通过loadedmetadata事件获取视频实际时长,自动调整占位容器高度,适配不同时长的视频文件。

内容的提问来源于stack exchange,提问作者ktt

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 02:17:40