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

Next.js 集成 Plyr.js 移动端视频控件异常问题

Next.js 集成 Plyr.js 移动端视频控件异常问题

嘿,我刚好踩过Plyr移动端控件闪屏的坑,你的分析完全精准——移动端浏览器会把首次触摸动作模拟成mouseover(也就是hover)+click两个事件,Plyr默认会在mouseover时弹出控件,紧接着click事件触发播放/暂停,同时控件又因为hover状态消失而隐藏,就造成了闪一下就没的尴尬。结合你给的Next.js 14 + Plyr 3.7.8代码,给你一套落地的解决方案:

问题根源拆解

  1. 移动端事件模拟机制:首次触摸会先后触发touchstart→mouseover→click,Plyr的控件逻辑在这两个事件间产生了冲突;
  2. 你的代码里有个小笔误:window.innerWidth < 500 || window.innerWidth < 500是重复判断,完全没必要;
  3. 自定义click事件和Plyr内置的播放/暂停逻辑叠加,进一步放大了冲突。

具体解决方案

1. 调整Plyr初始化配置,区分端侧行为

在初始化Plyr时,合并触摸和鼠标相关的配置,直接禁用不必要的事件模拟:

const plyr = new Plyr(`#${id}`, {
  ...video.options(),
  // 自定义触摸端行为
  touch: {
    controlsDelay: 0, // 禁用控件自动隐藏延迟(可按需调整毫秒数)
    clickToPlay: false, // 关闭Plyr内置的触摸点击播放,交给自定义逻辑处理
  },
  // 保留桌面端原生鼠标交互
  mouse: {
    clickToPlay: true,
    hover: true,
  },
});

2. 修正自定义Click事件逻辑,避免冲突

把容器的Click事件监听改成下面这样,修复笔误,同时精准区分触摸/鼠标触发的事件:

containerRef.current.addEventListener("click", (e) => {
  const target = e.target as HTMLElement;
  // 如果点击的是控件元素,直接跳过,不干扰Plyr原生控件逻辑
  if (
    target.parentElement?.classList.contains("plyr__controls") ||
    target.classList.contains("plyr__control") ||
    target.parentElement?.classList.contains("plyr__control")
  ) {
    return;
  }

  // 用更合理的移动端断点(这里用768px,可根据你的设计稿调整)
  const isMobile = window.innerWidth < 768;
  if (isMobile) {
    // 判断是否是触摸触发的Click事件
    const isTouchTriggered = e.detail === 0 || "ontouchstart" in window;
    if (isTouchTriggered) {
      plyr.togglePlay();
      // 手动强制显示控件,避免hover状态消失导致的隐藏
      plyr.elements.controls?.classList.add("plyr__controls--visible");
    }
  } else {
    // 桌面端保留原生点击播放逻辑
    plyr.togglePlay();
  }
});

3. (可选)CSS兜底,强制移动端控件状态

如果上面的配置还没完全解决问题,可以加一段全局CSS兜底:

@media (max-width: 767px) {
  .plyr--video.plyr--playing .plyr__controls {
    opacity: 1 !important;
    visibility: visible !important;
  }
}

完整修改后的组件代码

'use client'; // Next.js 14 App Router 必须加,客户端组件标记
import { ReactElement, VideoHTMLAttributes, useId, useRef } from "react";
import Plyr from "plyr";
import "plyr/dist/plyr.css";
import { useSubscriptionEffectV2 } from "~/utils/rxjs/useSubscriptionEffect";
import Video from "./Video";
import { LocalizationValue } from "~/components/business/localization/Locale";

type PlyrProps = { video: Video; } & VideoHTMLAttributes<HTMLVideoElement>;

// #LOCALE MARKER: use this to find locales across the codebase
const CaptionsLabelMap: Record<LocalizationValue, string> = {
  en: "English",
  ua: "Ukrainian",
  es: "Spanish",
  pt: "Portuguese",
  de: "German",
  fr: "French",
  it: "Italian",
  nl: "Dutch",
  da: "Danish",
  sv: "Swedish",
  fi: "Finnish",
  nb: "Norwegian",
} as const;

const PlyrItem = ({ video, ...videoProps }: PlyrProps): ReactElement => {
  const containerRef = useRef<HTMLDivElement>(null);
  const id = useId().slice(1, -1);

  useSubscriptionEffectV2(() => {
    const plyr = new Plyr(`#${id}`, {
      ...video.options(),
      touch: {
        controlsDelay: 0,
        clickToPlay: false,
      },
      mouse: {
        clickToPlay: true,
        hover: true,
      },
    });

    for (const [name, listener] of video.setupEventListeners(id).entries())
      plyr.on(name, listener);

    if (containerRef.current) {
      containerRef.current.addEventListener("click", (e) => {
        const target = e.target as HTMLElement;
        if (
          target.parentElement?.classList.contains("plyr__controls") ||
          target.classList.contains("plyr__control") ||
          target.parentElement?.classList.contains("plyr__control")
        ) {
          return;
        }

        const isMobile = window.innerWidth < 768;
        if (isMobile) {
          const isTouchTriggered = e.detail === 0 || "ontouchstart" in window;
          if (isTouchTriggered) {
            plyr.togglePlay();
            plyr.elements.controls?.classList.add("plyr__controls--visible");
          }
        } else {
          plyr.togglePlay();
        }
      });
    }

    return () => plyr.destroy();
  }, [video]);

  return (
    <div ref={containerRef}>
      <video
        id={id}
        playsInline
        controls
        crossOrigin="anonymous"
        src="https://videos.pexels.com/video-files/20363446/20363446-uhd_2560_1440_30fps.mp4"
        {...videoProps}
      >
        {!!video.captionsSrc() && (
          <track
            kind="captions"
            label={CaptionsLabelMap[video.captionsSrcLang()]}
            srcLang={video.captionsSrcLang()}
            src={video.captionsSrc()}
            default
          />
        )}
      </video>
    </div>
  );
};

export default PlyrItem;

额外注意点

  • 如果你用的是Next.js 14 App Router,必须在组件顶部加'use client'指令,因为Plyr需要操作DOM,不能在服务端渲染;
  • 测试时尽量用真实移动端设备,Chrome的模拟器和真实设备的事件触发逻辑可能有差异。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.08 14:08:07