
Chrome 125 中多个 标签的原生控制栏「三点菜单」(overflow menu)会统一锚定到页面最后一个视频元素,而非触发它的目标视频——该问题在 Chrome 126 已修复,但需兼容旧版时须手动干预。
chrome 125 中多个 `
在 React 应用中渲染多个 <video></video> 元素时,若依赖浏览器原生 controls(如 controls 属性),其内置的「三点菜单」(即右键菜单或点击控件栏右上角 ··· 弹出的选项菜单)在 Chrome 125 及更早版本中存在一个已知渲染缺陷:所有视频共享同一菜单 DOM 实例,且该实例的位置计算始终绑定到最后一个渲染的 <video></video> 元素。即使你为每个视频设置了唯一 id、key 和独立容器(如 videodiv-1、videodiv-2),也无法改变这一底层行为——因为该菜单并非由 React 管理,而是由 Chromium 渲染引擎在全局层面对 <video></video> 控件进行复用与定位。
✅ 验证与结论
该问题已被 Chromium 官方确认并修复(Chromium Issue #1457823),自 Chrome 126(2024年6月发布)起默认生效。你可在不同版本中快速验证:
# 查看当前 Chrome 版本 chrome://version/
若用户仍使用 Chrome 125 或更旧版本,且需确保菜单准确定位,则必须禁用原生 controls,转而实现自定义控制栏。这是目前唯一稳定、跨版本兼容的解决方案。
? 推荐实践:替换为自定义控制栏(轻量级示例)
以下代码移除了 controls 属性,用 React 状态 + 原生 API 实现播放/暂停及「三点菜单」按钮,并通过 useRef 精确获取触发按钮位置,确保浮层(如 Popover 或绝对定位菜单)正确对齐:
import React, { useRef, useState, useEffect } from 'react';
const VideoPlayer = ({ src, id }: { src: string; id: number }) => {
const videoRef = useRef<htmlvideoelement>(null);
const buttonRef = useRef<htmlbuttonelement>(null);
const [isPlaying, setIsPlaying] = useState(false);
const [menuPosition, setMenuPosition] = useState(null);
const [showMenu, setShowMenu] = useState(false);
const togglePlay = () => {
if (!videoRef.current) return;
if (isPlaying) {
videoRef.current.pause();
} else {
videoRef.current.play().catch(console.error);
}
setIsPlaying(!isPlaying);
};
const handleMenuClick = () => {
if (!buttonRef.current) return;
const rect = buttonRef.current.getBoundingClientRect();
setMenuPosition({
top: rect.bottom + window.scrollY,
left: rect.right - 120 + window.scrollX, // 向左偏移适配菜单宽度
});
setShowMenu(true);
};
useEffect(() => {
const handleClickOutside = () => setShowMenu(false);
document.addEventListener('click', handleClickOutside);
return () => document.removeEventListener('click', handleClickOutside);
}, []);
return (
<div classname="relative" id="{`videodiv-${id}`}">
<video ref="{videoRef}" classname="w-[230px] h-[250px] object-cover rounded-lg" id="{`video-${id}`}" src="%7Bsrc%7D" onplay="{()"> setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
/>
{/* 自定义控制栏 */}
<div classname="absolute bottom-2 right-2 flex items-center space-x-2 bg-black/70 rounded px-2 py-1">
<button ref="{buttonRef}" onclick="{handleMenuClick}" classname="text-white text-sm w-6 h-6 flex items-center justify-center hover:bg-white/20 rounded" aria-label="更多选项">
⋯
</button>
<button onclick="{togglePlay}" classname="text-white text-sm w-8 h-8 flex items-center justify-center hover:bg-white/20 rounded" aria-label="{isPlaying" :>
{isPlaying ? '⏸' : '▶'}
</button>
</div>
{/* 自定义三点菜单(绝对定位) */}
{showMenu && menuPosition && (
<div classname="absolute z-50 bg-gray-800 text-white rounded shadow-lg py-1 min-w-[120px] border border-gray-700" style="{{" top: left:>
<button classname="block w-full text-left px-4 py-2 hover:bg-gray-700 text-sm" onclick="{()"> {
videoRef.current?.requestPictureInPicture?.().catch(console.error);
setShowMenu(false);
}}
>
画中画
</button>
<button classname="block w-full text-left px-4 py-2 hover:bg-gray-700 text-sm" onclick="{()"> {
videoRef.current?.download && (window.location.href = src);
setShowMenu(false);
}}
>
下载
</button>
</div>
)}
</video>
</div>
);
};
const Test = () => {
const videos = [
{ src: '/icons/1.mov', id: 1 },
{ src: '/icons/2.mp4', id: 2 },
];
return (
<div classname="p-4">
{videos.map((video) => (
<videoplayer key="{video.id}" src="%7Bvideo.src%7D" id="{video.id}"></videoplayer>
))}
</div>
);
};
export default Test;</htmlbuttonelement></htmlvideoelement>
⚠️ 注意事项
- 不要尝试通过 CSS
z-index或transform修复原生菜单——它不受 React 或常规样式控制; - 若业务强依赖原生 controls(如字幕轨道、音轨切换等),需在自定义方案中调用
video.textTracks/video.audioTracks等 API 补全功能; - 对于 SSR 或服务端渲染场景,确保
useRef和事件监听仅在客户端执行(可加useEffect+typeof window !== 'undefined'判断); - 建议在应用启动时检测
navigator.userAgent并提示用户升级 Chrome(针对企业内网等无法自动更新的环境)。
总结:这不是代码逻辑错误,而是 Chromium 的临时渲染 Bug。拥抱自定义控制栏不仅解决定位问题,更能提升品牌一致性、无障碍支持(ARIA)与交互可控性——这也是现代 Web 视频体验的最佳实践。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











