新闻详情

新闻详情

首页 / 资讯中心 / 详情

video标签核心属性与事件详解:从duration到ended的播放器实战

发布时间:2026/10/1 11:50:07来源:尧图网络
video标签核心属性与事件详解:从duration到ended的播放器实战
搞前端这些年video 标签算是被问得最多的媒体标签没有之一。早两年接在线教育平台的项目十有八九的需求绕不开这几件事视频总时长怎么拿、当前播放到哪一秒了怎么实时更新、播完一集怎么自动切到下一集。后来帮朋友做 H5 游戏就是照着超级玛丽同人复刻版那种怀旧风video 的 ended 事件又成了关键动画剧情一结束就得切进游戏主场景玩过 Flappy Bird 的朋友也可以想象一下游戏开场放一段提示动画再用事件衔接全是这套逻辑。今天就把这些最常用的 video 属性和事件一次讲透最后附一个可以直接抄走的播放器 Demo代码量不大但足够撑起绝大多数业务场景。1. 先把 video 标签的几个核心属性摸清楚1.1 播放器的基本骨架长什么样一个最基础的 video 标签写出来还不到十行video idmyVideo src./assets/demo.mp4 controls preloadauto muted /videocontrols属性是让你直接用浏览器自带的那套控制条播放、暂停、进度拖拽都有了省事是真省事。但实际做项目时产品经理很少会让你一直顶着浏览器原生控件样式不统一、交互不可控尤其做在线教育或者视频站点时十有八九要自定义控制条。所以真正要做的是把 controls 拿掉自己用 JS 写一套控制逻辑。这时候 duration、currentTime、ended 这几个属性和背后的一堆事件就成了核心武器。其他几个常用内置属性也顺带说下preload告诉浏览器视频怎么预加载可选 none / metadata / auto。想快速拿到时长又不愿意浪费流量用 metadata。muted静音属性移动端自动播放政策下几乎是标配很多浏览器不允许带声音自动播放但静音播放可以。loop循环播放设置之后视频播完会自动从头开始。autoplay自动播放配合 muted 才更容易生效。1.2 duration、currentTime、ended 到底是个啥这三个是今天的主角先把概念掰扯清楚。duration视频总时长单位是秒带小数的浮点数。比如一段 1 分 30 秒的视频它的值可能是 90.036。它不是一上来就有值要等浏览器加载到媒体元数据metadata之后才能拿到准确数字初始状态下是 NaN。currentTime当前播放位置同样以秒为单位。它是可读可写的属性读取的时候返回当前播放到的位置写入的时候可以实现跳转播放比如video.currentTime 30就是把播放进度调到第 30 秒。这也是做进度条拖拽最核心的 API。ended播放是否已经结束一个只读的布尔值。当视频播放到最后一帧停下来后ended 变成 true同时会触发 ended 事件。注意一个细节如果设置了 loop视频不会停下来ended 基本不会触发。paused / muted / volume / playbackRate这几个也顺带提一下。paused 表示是否暂停经常用来判断当前该显示播放图标还是暂停图标muted 是静音开关volume 是音量取值 0 到 1playbackRate 是播放速率做倍速播放就靠它比如 2 就是两倍速。用表格整理一下方便后面翻查属性名含义可读写单位/取值备注duration视频总时长只读秒浮点数元数据加载完成后才有效currentTime当前播放位置可读可写秒浮点数写入即跳转ended是否播放结束只读true / false播放到结尾变为 truepaused是否暂停只读true / false常用于切换播放按钮状态muted是否静音可读可写true / false移动端自动播放常配合使用volume音量可读可写0 ~ 10 到 1 的浮点数playbackRate播放速率可读可写1 为正常速度2 为两倍速0.5 为半速1.3 为什么读 duration 之前要等 loadedmetadata很多新手上来就写console.log(video.duration)结果打出来是 NaN然后就开始怀疑人生。这不是代码写错了而是浏览器加载视频是分阶段的。你可以把视频理解成一本厚重的书浏览器先拿到的是封面和目录元数据里面写着这本书多少页、多少字然后才是正文内容一页页加载进来。duration 就是写在目录上的信息你连目录都没翻到自然读不到页数。标签一渲染出来就去读大概率什么都拿不到。要等浏览器触发loadedmetadata事件后再去读取这才能在第一时间拿到稳定准确的总时长。后面我写 Demo 的时候会再演示一次完整写法。2. 把时长和播放时间真正拿到手2.1 最稳的时长获取方式等 loadedmetadata获取总时长最标准的写法长这样const video document.getElementById(myVideo); video.addEventListener(loadedmetadata, function () { console.log(总时长秒:, video.duration); const totalMinutes Math.floor(video.duration / 60); const totalSeconds Math.floor(video.duration % 60); console.log(总时长: ${totalMinutes}:${totalSeconds.toString().padStart(2, 0)}); });loadedmetadata事件触发的时候浏览器已经拿到了视频的基本信息duration 这时就是准的后续基本不会变。把秒数换算成“分:秒”格式的操作是所有播放器页面都会写的工具函数建议直接封装成formatTime留着复用。这里有个实际开发中的细节loadedmetadata并不是 duration 可能变化的唯一时机。某些流媒体资源、动态生成的视频在加载过程中 duration 会更新这时候会触发durationchange事件。常规 MP4 文件不用担心这个但如果你做的项目要对接 HLS、DASH 这类流协议建议durationchange也监听一下在里面同步时长显示避免用户看到 00:00 的尴尬。video.addEventListener(durationchange, function () { if (!isNaN(video.duration)) { totalTimeEl.textContent formatTime(video.duration); } });2.2 currentTime 的读和写进度条和跳转都靠它读取当前播放时间最推荐的方式是监听timeupdate事件。这个事件会随着播放进度周期性触发浏览器会尽可能在视频帧更新时同步通知你频率大概每帧一次正常播放时每秒能触发好几次足够用来刷新进度条和时间文字。const currentTimeEl document.getElementById(currentTime); const progressBar document.getElementById(progressBar); video.addEventListener(timeupdate, function () { const current video.currentTime; currentTimeEl.textContent formatTime(current); const percent (current / video.duration) * 100; progressBar.style.width percent %; });currentTime的写入操作最典型的场景是点击进度条跳转。先算出鼠标点击位置占进度条总宽度的比例再用这个比例乘总时长最后赋值给currentTimeprogressWrap.addEventListener(click, function (e) { const rect progressWrap.getBoundingClientRect(); const ratio (e.clientX - rect.left) / rect.width; video.currentTime ratio * video.duration; });看起来简单但有几个关键点要留意。第一赋值currentTime之前最好确认duration不是 NaN 也不是 Infinity否则算出来的跳转目标就是非法值。第二跳转行为是异步的赋值后播放位置不会立刻更新而是等浏览器真正把数据加载到那个位置触发seeking然后seeked事件这时候才算跳转完成。所以如果要跳转后立刻做点什么别在赋值后马上读currentTime要在seeked回调里处理。2.3 ended 事件播放结束后的正确衔接姿势播放结束最直观的表现是屏幕停在最后一帧播放按钮变成可播放的状态同时ended属性变为 true全局触发ended事件。video.addEventListener(ended, function () { // 场景一显示重播按钮 replayBtn.style.display block; // 场景二自动切到下一集 loadNextEpisode(); // 场景三H5 游戏里从开场动画切进主场景 startGame(); });这个事件在业务里用途极广。在线教育里播完自动弹出下一集短视频场景里播完自动播下一条H5 游戏里用一段预告片做完开场播完就切进主流程。做超级玛丽同人复刻版那会儿我就是把 end 事件当作“剧情动画完成”的信号事件一触发就释放键盘控制、初始化关卡数据、演奏游戏背景音乐一套流程行云流水。有一个容易踩的坑想提前说明如果 video 标签设置了loop属性视频播完会静默地从头开始根本就不会触发 ended 事件。如果你一边做“播完自动切换”一边又在标签上留着 loop代码永远不生效。正确的做法是不要在 HTML 上写死 loop需要用循环播放的交互时在ended事件里手动调用video.play()这样既实现了循环效果又能自己在事件里加额外逻辑。3. 常用 video 事件清单与搭配场景3.1 一张表看懂所有关键事件video 标签的事件不少真正高频使用的大概十五六个。我把它们整理成了一张速查表按“加载与元数据”、“播放与状态”、“进度与缓冲”、“错误与异常”四类分组开发的时候直接查表就行。事件名触发时机最常用的场景loadstart开始加载媒体数据显示 loading 状态loadedmetadata元数据加载完成时长等可用获取 duration初始化 UIloadeddata当前帧数据加载完成准备播放前的界面提示canplay缓冲到可以开始播放隐藏 loading亮起播放按钮canplaythrough预计可以不停顿播完提前隐藏缓冲提示playplay() 被调用播放开始同步播放按钮状态playing真正开始播放从暂停或缓冲恢复隐藏 loading更新状态pause暂停事件触发切换播放/暂停图标ended播放到结尾播完切换、重播、下一集timeupdate播放位置更新约每秒多次更新进度条、当前时间progress浏览器加载媒体数据时更新缓冲进度条durationchangeduration 属性变化重新显示总时长waiting数据不足播放被阻塞显示 loadingseeking跳转开始seeking 属性变 true更新 UI 为跳转中seeked跳转完成seeking 变 false隐藏跳转提示volumechangevolume 或 muted 变化同步音量图标ratechangeplaybackRate 变化更新倍速按钮高亮error媒体加载或播放出错错误提示与容错stalled浏览器尝试拉取数据但未获取到提示网络异常或弱网emptied资源被清空如重置 src重置播放器状态实际开发中不需要把上面所有事件都用上但loadedmetadata、timeupdate、playing、pause、ended、waiting、error这七个基本每个播放器项目都会碰见。3.2 播放与暂停按钮的状态联动Promise 的坑要提前避控制播放暂停的按钮逻辑看起来是最简单的但很容易因为忽略 play() 的异步行为而出 bugplayBtn.addEventListener(click, function () { if (video.paused) { video.play(); playBtn.textContent 暂停; } else { video.pause(); playBtn.textContent 播放; } });这个写法在多数浏览器里没问题但在移动端或某些自动播放策略严格的环境里video.play()会返回一个 Promise如果播放失败比如没有用户手势、音量过大被策略拦截Promise 会 reject并且抛出一个 Unhandled Rejection 警告同步把按钮文字改成“暂停”就变成误报状态。更稳的写法是playBtn.addEventListener(click, function () { if (video.paused) { video.play().catch(function () { // 播放失败时恢复按钮状态并给出提示 playBtn.textContent 播放; showToast(播放失败请检查网络后重试); }); playBtn.textContent 暂停; } else { video.pause(); playBtn.textContent 播放; } });更稳妥的状态同步方式其实是完全依赖事件驱动。监听play事件把按钮切成“暂停图标”监听pause事件把按钮切成“播放图标”点击时只负责调用play()/pause()UI 的状态交给事件去改。这样任何情况下播放状态变化按钮都能自动对齐包括浏览器手势被拦截、被系统电话打断、被后台切走等边界情况。3.3 高频事件 timeupdate 的性能处理和 buffered 进度timeupdate是高频事件正常播放时一秒触发 4 到 66 次不等如果每次都直接操作 DOM叠加起来还是有点性能压力的。做进度条时我习惯用requestAnimationFrame做一个简单的节流事件来了先把目标时间存到一个变量里具体更新 UI 的操作放到下一帧再去执行避免一秒钟几十次的强制重排。let lastCurrentTime 0; video.addEventListener(timeupdate, function () { lastCurrentTime video.currentTime; requestAnimationFrame(updateUI); }); function updateUI() { currentTimeEl.textContent formatTime(lastCurrentTime); progressBar.style.width (lastCurrentTime / video.duration * 100) %; }更多时候还需要展示缓冲进度就是进度条上那条淡色的加载进度。这个要监听progress事件通过video.buffered对象去读取缓冲范围video.addEventListener(progress, function () { if (video.buffered.length 0) { const bufferedEnd video.buffered.end(video.buffered.length - 1); const percent bufferedEnd / video.duration * 100; bufferBar.style.width percent %; } });buffered是一个 TimeRanges 对象可能包含多段缓冲区间通常取最后一段的结束时间基本上就是当前浏览器已经下载到的位置。4. 完整播放器 Demo从零写一个带进度条的播放器4.1 先搭一个能跑的页面结构理论说了一大堆不如直接上能跑的 Demo。下面这个例子做到了三件事显示总时长和当前时间、播放/暂停切换、点击进度条跳转、倍速切换、播放结束提示。代码不复杂但覆盖了前面讲到的绝大多数细节。HTML 部分video idmyVideo src./assets/demo.mp4 preloadmetadata playsinline muted/video div classplayer-wrap div classprogress-wrap idprogressWrap div classbuffer-bar idbufferBar/div div classprogress-bar idprogressBar/div /div div classcontrols button idplayBtn播放/button span idcurrentTime00:00/span span//span span idtotalTime00:00/span select idspeedSelect option value11x 正常/option option value1.51.5x/option option value22x/option /select /div div idendedTip styledisplay:none;播放结束点击重播/div /div核心 JS 部分我把整个逻辑拆成三个模块工具函数、事件绑定、UI 更新。事件绑定和 UI 更新分开是为了后面加功能时不用去大改监听逻辑。const video document.getElementById(myVideo); const playBtn document.getElementById(playBtn); const currentTimeEl document.getElementById(currentTime); const totalTimeEl document.getElementById(totalTime); const progressWrap document.getElementById(progressWrap); const progressBar document.getElementById(progressBar); const bufferBar document.getElementById(bufferBar); const speedSelect document.getElementById(speedSelect); const endedTip document.getElementById(endedTip); // 工具函数秒数转 mm:ss function formatTime(seconds) { if (isNaN(seconds)) return 00:00; const m Math.floor(seconds / 60); const s Math.floor(seconds % 60); return ${m.toString().padStart(2, 0)}:${s.toString().padStart(2, 0)}; } // 1. 元数据加载完成后显示总时长 video.addEventListener(loadedmetadata, function () { totalTimeEl.textContent formatTime(video.duration); }); // 2. 播放进度更新 video.addEventListener(timeupdate, function () { const current video.currentTime; currentTimeEl.textContent formatTime(current); if (video.duration) { progressBar.style.width (current / video.duration * 100) %; } }); // 3. 缓冲进度 video.addEventListener(progress, function () { if (video.buffered.length 0) { const bufferedEnd video.buffered.end(video.buffered.length - 1); bufferBar.style.width (bufferedEnd / video.duration * 100) %; } }); // 4. 播放 / 暂停按钮 playBtn.addEventListener(click, function () { if (video.paused) { video.play().catch(() { playBtn.textContent 播放; }); } else { video.pause(); } }); // 5. 播放状态联动 video.addEventListener(play, function () { playBtn.textContent 暂停; }); video.addEventListener(pause, function () { playBtn.textContent 播放; }); // 6. 点击进度条跳转 progressWrap.addEventListener(click, function (e) { const rect progressWrap.getBoundingClientRect(); const ratio (e.clientX - rect.left) / rect.width; if (video.duration isFinite(video.duration)) { video.currentTime ratio * video.duration; } }); // 7. 倍速切换 speedSelect.addEventListener(change, function () { video.playbackRate parseFloat(this.value); }); // 8. 播放结束 video.addEventListener(ended, function () { endedTip.style.display block; playBtn.textContent 播放; }); endedTip.addEventListener(click, function () { video.currentTime 0; video.play(); endedTip.style.display none; });4.2 这个 Demo 里藏的几个细节值得展开讲讲第一video标签上我加了preloadmetadata意思是页面加载后先只拉元数据不急着下载整段视频内容。这样页面一进来就能显示总时长同时不会浪费额外流量。第二playsinline属性是给 iOS Safari 用的不加的话 iPhone 上视频动不动就全屏播放交互非常割裂。第三进度条跳转前我判断了isFinite(video.duration)这是为了过滤掉 duration 为 Infinity 的边界情况防止算出一个不可用的跳转位置。第四播放结束提示这一块点击“重播”时先用video.currentTime 0回到开头再调video.play()。这里有个细节手动设置 currentTime 后要等seeked事件触发再调 play 更稳妥但大多数场景下连续调用也能正常工作因为浏览器会把播放请求排在跳转后面。想在极端情况下做到万无一失可以把 play 放到 seeked 回调里执行。4.3 顺手可以扩容的玩法这个 Demo 是很基础的骨架往上面加东西非常顺。比如做倍速播放时除了playbackRate有的浏览器会出现画面快进但声音还正常的“声画不同步”错觉这时候可以监听ratechange事件在事件里刷新倍速按钮的高亮状态。再比如想加画中画功能一行代码就够了video.requestPictureInPicture();进入画中画之后页面切到别的 Tab 视频仍然悬浮展示做网课播放器的时候体验很好。还有全屏直接调用video.requestFullscreen()就行跟普通 DOM 全屏逻辑一致。5. 常见问题与排查技巧实录5.1 视频时长要么拿不到要么显示 Infinityduration返回 Infinity这个坑大概率是遇到了流媒体资源比如没配置范围的 MP4 或某些直播流浏览器无法预知总时长。解决办法是区分场景如果是点播文件检查服务器是否正确响应 Range 请求视频文件建议用 Nginx 等静态服务器托管并开启 Range 支持如果本身就是直播或无限流界面就不要显示总时长改成“直播中”之类的标识。代码层面可以用一下判断兜底if (isFinite(video.duration)) { totalTimeEl.textContent formatTime(video.duration); } else { totalTimeEl.textContent 直播中; }5.2 currentTime 赋值了但跳不过去赋值video.currentTime 100后没有反应排查方向有三步第一步确认duration不是 NaN跳转目标不能超过总时长第二步确认视频不是用MediaSource动态追加数据的流这类资源如果缓冲区没有对应位置的数据跳转会卡住或自动失效第三步检查有没有别的事件把 currentTime 又重置了比如emptied事件触发时资源被清空所有播放状态都会复位。5.3 ended 一直不触发优先查三件事标签上有没有写死loop属性视频有没有真的播到最后一帧比如中途卡住了就没法触发是不是用了MediaStream这种实时流实时流不存在“播完”的概念。还有一种情况是写代码时把ended事件绑定放在了src赋值之前并且绑的是同一个函数但作用域里变量还没初始化导致报错中断。做视频组件时建议把事件绑定逻辑统一在一个initEvents函数里video 元素挂载完成后立即调用。5.4 移动端视频播放的一堆“怪癖”安卓和 iOS 对 video 的自动播放策略不尽相同但有一条通用规律带声音的自动播放大概率被拦截静音自动播放几乎都能过。很多运营活动页要求“进页面自动播放宣传视频”稳妥方案是标签上先muted等用户点击后或播放进行中再取消静音。还有一个坑是安卓某些浏览器在视频离开视口后会自动画中画需要监听leavepictureinpicture之类的事件做兼容处理这个不同浏览器行为差异比较大设计页面时留好交互兜底。5.5 页面卡顿别让小视频拖垮了主线程前面讲过timeupdate的高频特性。如果页面上同时有好几个视频或者视频播放之外还要做复杂的动画高频刷新 DOM 很容易造成掉帧。建议统一用requestAnimationFrame节流并且把 progress 节流到一秒最多一两次。另一个容易忽略的点是 CSS 里大量使用width加百分比布局progress 条更新会触发 Layout数据量上来后性能就崩了可以改用transform: scaleX()来做进度条伸缩性能会好很多。5.6 事件绑定的常见野路子有些同学为了方便直接在 HTML 标签上写onendedmyFun()这种做法在当前项目里快速验证可以但复用到工程化项目就很别扭全局函数污染、作用域混乱、难以移除监听。建议统一用addEventListener。如果用了 Vue 或 React注意在组件卸载时移除事件监听或者直接依赖框架的生命周期钩子去管理绑定和解绑否则会带来重复触发的隐患。最后聊几句实在的说句大实话video 标签的官方文档一页纸就能翻完属性事件也就那些但真到项目里要踩的坑一点不比业务逻辑少。我体会最深的一点是不要把 video 当黑盒凡是和 UI 状态同步相关的全都以事件为准别自己开定时器去轮询属性。浏览器已经替你调度好了播放时机、缓冲节奏和事件触发顺序你非要再造一套轮子只会给自己添堵。再分享一个小技巧做 H5 游戏开发的朋友如果要用 video 当开场动画记得在ended触发后调用一次video.pause()再切场景有些浏览器播放结束后会停在最后一帧并保留解码资源提前释放内存能让游戏主场景加载更快。真要把这套东西玩出更多花样可以试试把 video 和 Canvas 结合每帧把视频画面 draw 到 Canvas 里做滤镜特效那又是另一个大坑了。总之先把基础属性和事件吃透后面扩展什么都稳。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

数据库被注入木马后恢复:用TaoToken统一Key排查异常连接与数据回滚 2026/10/1 13:26:38

数据库被注入木马后恢复:用TaoToken统一Key排查异常连接与数据回滚

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
RK3576启动链深度解析:Maskrom与Loader协同机制 2026/10/1 13:26:38

RK3576启动链深度解析:Maskrom与Loader协同机制

1. 项目概述:RK3576“变砖”不是玄学,是启动链上某个环节的彻底失联你手里的RK3576开发板突然不亮灯、不识别USB、串口无任何输出——连最基础的AT指令都喂不进去,烧写工具报错“device not found”或“no response”,这时候圈内人…

阅读更多 →
EtherCAT与FSoE实战:从分布式时钟同步到安全通信,以H5U带24轴为例 2026/10/1 13:26:37

EtherCAT与FSoE实战:从分布式时钟同步到安全通信,以H5U带24轴为例

说句实在话,EtherCAT 这个名字在工控圈里已经不算新鲜了,但真正把它吃透的人并不多。很多做 PLC 的老工程师最开始对它的态度是怀疑的——以太网嘛,传传文件、连个电脑还行,拿来控制伺服轴,周期能稳吗?直到…

阅读更多 →
01背包压维实战:从二维MLE到一维倒序,彻底解决空间与效率问题 2026/10/1 13:26:31

01背包压维实战:从二维MLE到一维倒序,彻底解决空间与效率问题

先问你一个问题:如果一道01背包题目的物品数量是5000,背包容量是10000,你会怎么写状态数组?很多人的第一反应还是dp[5001][10001],然后提交,然后MLE。即使内存侥幸过关,时间也往往卡在超时边缘。…

阅读更多 →
航拍校园操场人体检测:YOLO数据集构建与训练全流程实战 2026/10/1 13:26:31

航拍校园操场人体检测:YOLO数据集构建与训练全流程实战

1. 航拍视角下的人体检测,到底难在哪里先把场景说清楚。航拍校园操场人体检测,指的是用无人机或者高位固定摄像头,从几十米到上百米的高度俯拍操场、跑道、球场这类开阔场地,然后在画面里把每一个人框出来。听起来跟普通的目标检测…

阅读更多 →
TongWeb 7.0.4.9企业版Linux安装部署与License激活实战 2026/10/1 13:26:31

TongWeb 7.0.4.9企业版Linux安装部署与License激活实战

TongWeb 在不少单位的软件清单里属于必备件,尤其是近两年做系统迁移和中间件国产化替换的项目,几乎绕不开它。这次我拿到的是 TongWeb 7.0.4.9 企业版,操作系统是 Linux 服务器。很多刚接触这套环境的同事第一反应是“这不就是个 tomcat 吗”…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉