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

React多视频自定义进度条异常:播放第二个视频同步更新前一个

问题:React视频进度条更新异常

我正在开发一个React应用,在界面中展示三个视频,每个视频对应一个自定义进度条。目前遇到进度条更新异常问题:第一个视频播放完成时进度条正常,但启动第二个视频播放后,会同时更新第一个和第二个视频的进度条。我使用timeupdate事件来更新视频进度条。

原代码

JavaScript

import * as React from 'react';
import './style.css';

export default function App() {
  var v3 = `http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4`;
  var v2 = `http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4`;
  var v1 = `http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4`;

  const [id, setId] = React.useState(1);
  const [currentVideo, setCurrentVideo] = React.useState('');
  const [barCompleted, setBarCompleted] = React.useState<any>({
    myBar1: false,
    myBar2: false,
    myBar3: false,
  });
  const videoRef = React.useRef<any>();
  const progress1 = React.useRef<any>();
  const progress2 = React.useRef<any>();
  const progress3 = React.useRef<any>();
  const myBar1 = React.useRef<any>();
  const myBar2 = React.useRef<any>();
  const myBar3 = React.useRef<any>();

  const handleOnEnded = () => {
    videoRef.current.removeEventListener('timeupdate', () => {});
    // setBarCompleted(prev=>{myBar1:true,...prev})
    setId((prev) => prev + 1);
  };

  const getVideoById = (id) => {
    if (id == 1) {
      setCurrentVideo(v1);
    } else if (id == 2) {
      setCurrentVideo(v2);
    } else if (id == 3) {
      setCurrentVideo(v3);
    }
  };

  React.useEffect(() => {
    getVideoById(id);
  }, [id]);

  React.useEffect(() => {
    videoRef?.current?.load();
    configureProgress(id);
  }, [currentVideo, id]);

  const configureProgress = (id) => {
    videoRef?.current?.addEventListener('timeupdate', (e) => {
      // console.log(e.target.currentTime);
      let i = 0;
      if (i == 0) {
        i = 1;
        // var elem = document.getElementById('myBar1');
        let width = 1;
        let sid = setInterval(frame, 1000);
        function frame() {
          if (videoRef.current.ended || width >= 100) {
            clearInterval(sid);
            i = 0;
            if (id == 1) {
              progress1.current.style.backgroundColor = 'green';
              myBar1.current.style.width = 0 + '%';
            } else if (id == 2) {
              progress2.current.style.backgroundColor = 'green';
              myBar2.current.style.width = 0 + '%';
            } else if (id == 3) {
              progress3.current.style.backgroundColor = 'green';
              myBar3.current.style.width = 0 + '%';
            }
          } else {
            width = Math.ceil(
              (videoRef.current.currentTime / videoRef.current.duration) * 100
            );
            if (id == 1) {
              myBar1.current.style.width = width + '%';
              console.log('width update id1');
            } else if (id == 2) {
              myBar2.current.style.width = width + '%';
              console.log('width update id2');
            } else if (id == 3) {
              myBar3.current.style.width = width + '%';
              console.log('width update id3');
            }
          }
        }
      }
    });
  };

  return (
    <div>
      <div className="progress-container">
        <div id="myProgress1" ref={progress1}>
          <div id="myBar1" ref={myBar1}></div>
        </div>
        <div id="myProgress2" ref={progress2}>
          <div id="myBar2" ref={myBar2}></div>
        </div>
        <div id="myProgress3" ref={progress3}>
          <div id="myBar3" ref={myBar3}></div>
        </div>
      </div>
      {currentVideo && (
        <video
          autoPlay={true}
          controls
          muted={true}
          id="myVideo1"
          width={300}
          height={250}
          ref={videoRef}
          crossOrigin="anonymous"
          onEnded={handleOnEnded}
          preload="metadata"
          playsInline
        >
          <source src={currentVideo} type="video/mp4" />
        </video>
      )}
    </div>
  );
}

CSS

.progress-container {
  display: flex;
}
#myProgress1,
#myProgress2,
#myProgress3 {
  width: 33%;
  background-color: #ddd;
}

#myBar1,
#myBar2,
#myBar3 {
  width: 1%;
  height: 30px;
  background-color: #04aa6d;
}

解决方案

问题根源

  1. 旧监听器未清理:每次切换视频都会新增timeupdate监听器,旧监听器未被移除,导致多个监听器同时触发更新不同进度条。
  2. 监听器移除无效:handleOnEnded中传入的是新匿名函数,和之前添加的监听器不是同一引用,无法完成移除。
  3. 定时器冗余:timeupdate事件本身会随播放进度触发,嵌套定时器属于多余逻辑,还会导致进度更新延迟。

修复后的代码

import * as React from 'react';
import './style.css';

export default function App() {
  const v3 = `http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4`;
  const v2 = `http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4`;
  const v1 = `http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4`;

  const [id, setId] = React.useState(1);
  const [currentVideo, setCurrentVideo] = React.useState('');
  const videoRef = React.useRef<HTMLVideoElement>(null);
  
  // 统一管理进度条和进度块的ref
  const progressRefs = {
    1: React.useRef<HTMLDivElement>(null),
    2: React.useRef<HTMLDivElement>(null),
    3: React.useRef<HTMLDivElement>(null),
  };
  const barRefs = {
    1: React.useRef<HTMLDivElement>(null),
    2: React.useRef<HTMLDivElement>(null),
    3: React.useRef<HTMLDivElement>(null),
  };
  
  // 保存当前监听器引用,用于移除操作
  const timeUpdateListenerRef = React.useRef<((e: Event) => void) | null>(null);

  const getVideoById = (videoId: number) => {
    switch(videoId) {
      case 1:
        setCurrentVideo(v1);
        break;
      case 2:
        setCurrentVideo(v2);
        break;
      case 3:
        setCurrentVideo(v3);
        break;
      default:
        break;
    }
  };

  const handleOnEnded = () => {
    // 移除当前timeupdate监听器
    if (videoRef.current && timeUpdateListenerRef.current) {
      videoRef.current.removeEventListener('timeupdate', timeUpdateListenerRef.current);
      timeUpdateListenerRef.current = null;
    }
    // 标记当前进度条完成
    if (progressRefs[id].current) {
      progressRefs[id].current.style.backgroundColor = 'green';
    }
    // 切换到下一个视频(最多3个)
    if (id < 3) {
      setId(prev => prev + 1);
    }
  };

  // 直接在timeupdate事件中更新进度
  const updateProgress = (videoId: number) => {
    if (!videoRef.current) return;
    const { currentTime, duration } = videoRef.current;
    if (duration === 0) return;
    
    const width = Math.ceil((currentTime / duration) * 100);
    if (barRefs[videoId].current) {
      barRefs[videoId].current.style.width = `${width}%`;
    }
  };

  React.useEffect(() => {
    getVideoById(id);
  }, [id]);

  React.useEffect(() => {
    if (!videoRef.current || !currentVideo) return;

    // 先清理之前的监听器
    if (timeUpdateListenerRef.current) {
      videoRef.current.removeEventListener('timeupdate', timeUpdateListenerRef.current);
    }

    // 加载新视频
    videoRef.current.load();

    // 创建并保存新的监听器
    const handleTimeUpdate = () => updateProgress(id);
    timeUpdateListenerRef.current = handleTimeUpdate;
    videoRef.current.addEventListener('timeupdate', handleTimeUpdate);

    // 组件卸载或切换视频时清理监听器
    return () => {
      if (videoRef.current && timeUpdateListenerRef.current) {
        videoRef.current.removeEventListener('timeupdate', timeUpdateListenerRef.current);
        timeUpdateListenerRef.current = null;
      }
    };
  }, [currentVideo, id]);

  return (
    <div>
      <div className="progress-container">
        <div id="myProgress1" ref={progressRefs[1]}>
          <div id="myBar1" ref={barRefs[1]}></div>
        </div>
        <div id="myProgress2" ref={progressRefs[2]}>
          <div id="myBar2" ref={barRefs[2]}></div>
        </div>
        <div id="myProgress3" ref={progressRefs[3]}>
          <div id="myBar3" ref={barRefs[3]}></div>
        </div>
      </div>
      {currentVideo && (
        <video
          autoPlay={true}
          controls
          muted={true}
          width={300}
          height={250}
          ref={videoRef}
          crossOrigin="anonymous"
          onEnded={handleOnEnded}
          preload="metadata"
          playsInline
        >
          <source src={currentVideo} type="video/mp4" />
        </video>
      )}
    </div>
  );
}

关键改进

  1. 统一Ref管理:用对象整合进度条和进度块的ref,简化逻辑调用。
  2. 监听器引用保存:通过timeUpdateListenerRef保存当前监听器,确保切换视频时能彻底移除旧监听器。
  3. 清理副作用:在useEffect的返回函数中添加监听器清理逻辑,避免内存泄漏。
  4. 简化进度更新:去掉冗余定时器,直接在timeupdate事件回调中计算并更新进度条,保证实时性。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.11 01:35:24