Next.js 集成 Plyr.js 移动端视频控件异常问题
Next.js 集成 Plyr.js 移动端视频控件异常问题
嘿,我刚好踩过Plyr移动端控件闪屏的坑,你的分析完全精准——移动端浏览器会把首次触摸动作模拟成mouseover(也就是hover)+click两个事件,Plyr默认会在mouseover时弹出控件,紧接着click事件触发播放/暂停,同时控件又因为hover状态消失而隐藏,就造成了闪一下就没的尴尬。结合你给的Next.js 14 + Plyr 3.7.8代码,给你一套落地的解决方案:
问题根源拆解
- 移动端事件模拟机制:首次触摸会先后触发
touchstart→mouseover→click,Plyr的控件逻辑在这两个事件间产生了冲突; - 你的代码里有个小笔误:
window.innerWidth < 500 || window.innerWidth < 500是重复判断,完全没必要; - 自定义
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
相关产品推荐
相关产品推荐

