不能直接用<video><audio>标签写样式,因为其controls控件由Shadow DOM封装,普通CSS无法穿透;需移除controls,用HTML+JS自定义控制逻辑,并通过事件监听和状态同步确保UI与媒体状态一致。

为什么不能直接用 <video> 或 <audio> 标签写样式
浏览器对原生 <video> 和 <audio> 控件的内部结构做了封装,controls 属性启用后渲染的按钮、进度条等是 Shadow DOM 内部节点,普通 CSS 无法穿透选择(除非用 ::-webkit-media-controls 这类非标准伪元素,但兼容性差、行为不稳定)。真正可控的起点,不是美化已有控件,而是关掉它,自己用 HTML + JS 构建控制逻辑。
必须关闭原生控件并暴露媒体元数据
自定义样式的第一步是剥离浏览器默认 UI,同时确保能读取和操作播放状态、时间、音量等。关键在于:controls 属性必须移除,且需监听 loadedmetadata、timeupdate、play、pause 等事件。
-
<video id="myVideo" preload="metadata"><source src="demo.mp4" type="video/mp4"></video>—— 不加controls,但保留preload="metadata"保证时长等信息尽早可用 - JavaScript 中调用
myVideo.duration、myVideo.currentTime、myVideo.volume等属性前,要等loadedmetadata事件触发,否则值为NaN或0 - 播放/暂停不能只靠
myVideo.play()/myVideo.pause(),还要同步更新自定义按钮的文本或图标状态(比如按钮 innerHTML 从 “▶” 切换到 “⏸”)
进度条拖拽需要手动绑定 input 和 change 事件
原生 <input type="range"> 拖动时会触发 input(实时)和 change(结束),但媒体元素的 currentTime 设置有延迟或校准误差,直接赋值可能跳转不准,尤其在 HLS 或低性能设备上。
- 监听
range的input事件时,先用video.duration * event.target.value / 100计算目标时间,再设video.currentTime = targetTime - 但必须配合
seeking和seeked事件:拖动中触发seeking,完成后触发seeked;可在seeked后重新同步 range 的 value,避免视觉错位 - 若视频未加载完成(
readyState < 1),强行设置currentTime可能无效,此时应缓存目标值,在canplay后再执行
音量与静音状态需双向同步处理
<input type="range"> 控制音量时,video.volume 是 0–1 的浮点数,而 range 的 value 默认是字符串,且静音状态(video.muted)和音量数值是两个独立开关,UI 上常合并为一个滑块+静音图标,逻辑容易混乱。
立即学习“前端免费学习笔记(深入)”;
- 初始时,用
range.value = Math.round(video.volume * 100)同步音量值;设置video.muted = false,否则即使 volume > 0 也无声 - 点击静音图标时,仅切换
video.muted,不改volume值;拖动滑块时,设video.volume = range.value / 100并同时置video.muted = false - 监听
volumechange事件——它会在用户调系统音量、插拔耳机、甚至某些安卓 WebView 中静音时触发,必须据此反向更新 UI
真正的难点不在写几个 div 和 click 绑定,而在所有状态变更都必须有明确来源(用户操作 / 媒体事件 / 系统干预)和统一出口(更新 DOM + 更新 media element),漏掉任意一条链路,UI 就会和实际状态脱节。



















