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

React中实现MP4视频无缝连续播放:解决从服务器更新视频URL时的画面闪烁问题

React中实现MP4视频无缝连续播放:解决从服务器更新视频URL时的画面闪烁问题

我完全理解你遇到的困扰——直接切换<video>的src属性时,浏览器需要重新请求并加载新视频,中间会出现短暂的空白或闪烁,严重破坏了用户体验。下面我会结合你的代码,给出几个针对性的解决方案,帮你实现真正无缝的视频过渡效果。

核心问题分析

你当前的实现是通过修改单个video元素的src来切换视频,这种方式的本质是销毁旧的视频流并重新初始化新的流,浏览器需要时间下载、解码新视频数据,这就是闪烁的根源。要实现无缝切换,关键是让新视频在切换前就完成预加载,切换时只做显示状态的切换,而非重新加载。

方案一:双视频元素预加载(最可靠的无缝方案)

这是视频无缝切换的行业标准做法:同时渲染两个<video>元素,一个用于显示当前正在播放的视频,另一个预加载下一个要播放的视频。当预加载完成后,直接切换两者的显示状态,完全避免加载空白。

针对你的代码修改示例

import { Button } from '@/components/ui/button';
import { useEffect, useState, useRef } from 'react';

interface DidAvatarProps {
  resultUrl?: string;
  handleCallDisconnect: () => void;
  onAudioCompletion: () => void;
}

const DidAvatar = ({ resultUrl, handleCallDisconnect, onAudioCompletion }: DidAvatarProps) => {
  const idleVideo = '/assets/didAnimation/maryIdle.mp4';
  // 控制当前显示的视频源和预加载的视频源
  const [activeSrc, setActiveSrc] = useState(resultUrl || idleVideo);
  const [preloadSrc, setPreloadSrc] = useState<string | null>(null);
  // 视频元素引用,用于监听加载状态
  const activeVideoRef = useRef<HTMLVideoElement>(null);
  const preloadVideoRef = useRef<HTMLVideoElement>(null);

  // 监听resultUrl变化,预加载新视频
  useEffect(() => {
    if (!resultUrl) {
      // 回到空闲视频,直接切换(本地资源无需预加载)
      setActiveSrc(idleVideo);
      setPreloadSrc(null);
      return;
    }

    // 如果新视频不是当前正在播放的,启动预加载
    if (resultUrl !== activeSrc) {
      setPreloadSrc(resultUrl);

      // 等待预加载视频完成缓冲后,再切换显示
      const handleCanPlayThrough = () => {
        setActiveSrc(resultUrl);
        setPreloadSrc(null);
        preloadVideoRef.current?.removeEventListener('canplaythrough', handleCanPlayThrough);
      };

      preloadVideoRef.current?.addEventListener('canplaythrough', handleCanPlayThrough);
      preloadVideoRef.current?.load(); // 触发视频预加载

      // 清理函数:移除事件监听
      return () => {
        preloadVideoRef.current?.removeEventListener('canplaythrough', handleCanPlayThrough);
      };
    }
  }, [resultUrl, activeSrc]);

  // 处理视频播放结束事件
  const handleVideoEnded = () => {
    // 预加载空闲视频,完成后切换
    setPreloadSrc(idleVideo);
    const handleIdleReady = () => {
      setActiveSrc(idleVideo);
      setPreloadSrc(null);
      onAudioCompletion();
      preloadVideoRef.current?.removeEventListener('canplaythrough', handleIdleReady);
    };

    preloadVideoRef.current?.addEventListener('canplaythrough', handleIdleReady);
    preloadVideoRef.current?.load();
  };

  return (
    <div className='w-full h-full relative'>
      <div className='bg-slate-blue-medium/20 w-96 h-96 blur-[191px] absolute -top-52 -right-44'></div>
      <div className='bg-purple-medium/15 w-96 h-96 blur-[190px] absolute -bottom-52 -left-44'></div>
      <div className='w-full h-full flex justify-center items-center relative'>
        {/* 当前正在显示的视频 */}
        <video
          ref={activeVideoRef}
          src={activeSrc}
          autoPlay
          playsInline
          loop={activeSrc === idleVideo}
          onEnded={activeSrc !== idleVideo ? handleVideoEnded : undefined}
          className='rounded-full absolute inset-0 w-full h-full object-contain'
        />
        {/* 预加载的视频(隐藏状态,仅用于缓冲数据) */}
        {preloadSrc && (
          <video
            ref={preloadVideoRef}
            src={preloadSrc}
            preload='auto'
            playsInline
            className='rounded-full absolute inset-0 w-full h-full object-contain opacity-0 pointer-events-none'
          />
        )}
        <div className='w-full h-fit p-2 absolute bottom-0'>
          <Button className='bg-purple-medium hover:bg-purple-medium absolute bottom-5'>
            First video
          </Button>
        </div>
      </div>
    </div>
  );
};

export default DidAvatar;

关键细节说明

  1. 预加载触发时机:当resultUrl变化时,先将新视频URL赋值给隐藏的预加载视频,监听它的canplaythrough事件(表示视频已缓冲足够数据,可流畅播放至结束),之后再切换为显示状态。
  2. 隐藏视频的样式处理:使用opacity: 0而非display: none,因为display: none会让浏览器跳过预加载逻辑;pointer-events: none避免隐藏视频干扰用户交互。
  3. 空闲视频优化:由于空闲视频是本地资源,加载速度极快,可直接切换无需预加载,简化逻辑。

方案二:利用浏览器缓存与预加载(简化方案)

如果你的视频URL会被重复请求,可以依赖浏览器的缓存机制,提前加载视频到内存中,再切换src。这种方案代码更简洁,但可靠性不如双视频方案(受浏览器缓存策略影响)。

代码示例

// 在组件中添加预加载逻辑
const preloadVideo = useRef<HTMLVideoElement | null>(null);

useEffect(() => {
  if (resultUrl && resultUrl !== videoSrc) {
    // 创建临时视频元素预加载
    preloadVideo.current = new Video();
    preloadVideo.current.src = resultUrl;
    preloadVideo.current.preload = 'auto';
    // 加载完成后再切换src
    preloadVideo.current.oncanplaythrough = () => {
      setVideoSrc(resultUrl);
      preloadVideo.current = null;
    };
  }
}, [resultUrl, videoSrc]);

方案三:Canvas绘制视频(高可控性方案)

如果需要更精细的画面控制(比如淡入淡出过渡),可以将视频帧绘制到Canvas上,切换时通过Canvas实现平滑过渡效果。这种方案复杂度较高,适合有高级视觉需求的场景。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.07 06:44:31