1. 先搞清楚:为什么视频不能直接塞进网页1.1 从GIF到HTML5视频的演进前几年还经常能看到有人用GIF做演示动效,一个十几秒的录屏转成GIF之后动辄几十MB,加载起来页面卡得不行。GIF本质上是图像格式,不是为连续画面临时设计的,位深只有8位,色彩断层严重,声音更是完全没戏。所以当你在HTML里需要播放一段真正带声音、色彩饱满的活动画面时,用GIF是“能用但极其痛苦”的兜底方案。HTML5标准在多年前就把<video>标签推进了规范,让浏览器原生支持视频播放,不需要任何插件。这个改变解决了三个痛点:一是文件体积比GIF小一个数量级,二是支持音频轨,三是播放控制能力(进度、声音、倍速)全部交给浏览器和JavaScript,开发者只负责给数据源和配置。这里要特别澄清一个误区:<video>标签并不是“把视频嵌入到网页里”,而是“让浏览器在文档中开辟一块独立区域,用它自己内核的解码器去拉取并渲染视频流”。这段视频始终是外部文件,HTML只是声明了它的位置和播放方式。1.2 视频格式与浏览器的“语言体系”为什么要先讲格式?因为HTML里的视频能不能播,代码写得再花哨都没用,浏览器不认识格式就摆一张裂开的图标。视频文件本身是一个容器,容器里装的是视频轨、音频轨和字幕轨,而视频轨又用不同的编码算法压缩。麻烦在于,不同浏览器支持的容器和编码组合不一样。迄今为止兼容性最好的组合是MP4容器配合H.264视频编码和AAC音频编码,几乎所有现代浏览器都能播。老牌企业级浏览器还对WebM(VP8/VP9编码)有一定支持,但覆盖率不如MP4。还有少部分场景会用到Ogg/Theora,基本被边缘化了。我给出的核心结论是:做网页视频,请无条件优先产出MP4/H.264/AAC格式;追求更高质量可以用H.265(HEVC),但Safari和部分移动端支持好,Windows上的Chrome和Firefox可能会直接拒绝播放,所以跨平台项目要谨慎。实操中我通常会额外准备一份WebM版本,专门喂给Chrome和Firefox,既能用VP9把画质做高,体积还比H.264更小。提示:视频格式不是看扩展名,而是看容器内的编码信息。你完全可以拿到一个后缀为.mp4的文件,但里面视频轨是HEVC,结果在某个浏览器上死活播放不了。2. 揭晓主角:video标签的完整骨架2.1 最简用法:一行代码真的能播视频如果你手里已经有一个MP4文件,路径正确,网络正常,那么下面这段代码确实能跑:<video src="./demo.mp4" controls></video>controls属性决定浏览器是否显示原生控制条,包括播放/暂停按钮、进度条、音量、全屏入口。不加它,视频区就是一块“哑巴画面”,用户点哪儿都没反应,只能通过JavaScript触发播放。如果连src都没有,那video标签就变成一块空区域,高度塌陷成0,在页面上什么也看不到。实操层面每个人都要养成习惯:写上controls只是怒下限,不要把原生控制条当成最终UI。尤其面向C端产品时,原生控制条在不同平台上的观感差异极大(Windows上Chrome的样式和iOS Safari的样式完全不同),真要打磨体验,后面第5节会用JavaScript做自定义控制层。2.2 单标签替代方案:source多源设置src属性直给一个文件是简单场景。真正的线上项目里,为了兼顾画质和兼容性,我会放多个格式的源文件。这时就不能再用src属性,而是把video标签写空,里面逐个塞<source>子标签:<video controls> <source src="demo.mp4" type="video/mp4"> <source src="demo.webm" type="video/webm"> </video>浏览器拿到video标签后,会从上到下依次检查source的type是否在自己支持列表里。如果支持就拿它播,不支持就跳过看下一个。若所有source都不支持,整个视频区域显示空白,并且触发video的 error 事件。这里有个隐藏好处:type属性不仅告诉浏览器“我是什么格式”,还能让它第一时间跳过完全不可能支持的源,减少无效请求。你不写type它也是能工作的,浏览器会下载文件头部一小段,嗅探真实格式再决定;但写了更规范,也能省掉不必要的内容协商。2.3 关键属性逐个掰开揉碎autoplay:页面加载后自动开始播放。但这条属性在大多数浏览器里默认无效,因为浏览器禁止带声音的视频自动播放,防止打开一堆页面后背景里突然有人说话吓人一跳。解决办法是同时加muted属性(静音播放),并且最好再加上playsinline(iOS Safari上不允许视频自动全屏播放必备)。loop:循环播放。做背景视频、免打扰动效时配合muted+autoplay使用。poster:视频加载完或播放前的占位封面图。不设置就是第一帧画面,有些情况第一帧是黑屏或杂乱画面,体验很差,建议给一张有设计感的海报图。preload:告诉浏览器要不要预加载视频数据。可选none、metadata、auto。none就是不预下载,等到用户点播放再请求;metadata只下载元数据(时长、尺寸、首帧);auto尽量预加载整个文件。移动端流量敏感,建议默认metadata或none。width/height:设置视频区域宽高。建议只定其中一个,另一个由浏览器按视频原始宽高比自动计算;两个都定但比例不一致时,视频会变形拉伸,画面里的人会变成矮胖版或瘦长版。crossorigin:当你的视频用来做Canvas离屏处理、或者从跨域CDN拉取时,需要设置它才能让浏览器认为“这是一个合法的跨域资源”。普通播放不需要加。disablepictureinpicture:关闭画中画按钮。不是所有浏览器都认,但新版Chrome支持良好。如果你不希望用户把视频拖成小窗一直霸占屏幕,加上它。controlslist:精细控制原生控制条上显示哪些按钮,比如不显示下载按钮、不显示全屏按钮。它是不完全标准的属性,在Chrome系有效,Safari就不完全响应。3. 实操:从零到一在页面里播放视频3.1 第一步:准备素材,确定编码视频源从哪里来?可能是剪辑软件导出的成片、手机拍的竖屏短视频、也可能是服务端自动转码后给出的流地址。不管来源,只要最终推到网页上的成品,我都建议过一遍转码流程。转码工具我用的是ffmpeg命令行,只要电脑装了ffmpeg,就可以固定用下面这套参数生产Web兼容性最好的MP4:ffmpeg -i input.mov -c:v libx264 -profile:v high -level 4.0 \ -crf 23 -preset medium -c:a aac -b:a 128k \ -movflags +faststart -pix_fmt yuv420p output.mp4逐项解释这些参数,因为每一个都踩过坑:-profile:v high -level 4.0:H.264编码规范里的档位和级别。high档画质好,4.0对应最大分辨率到1080p级别,能覆盖绝大多数浏览器硬解需求。不要用baseline,那是旧设备级别,画质损失明显。-crf 23:恒定质量参数,23是某位平衡值,数字越小画质越好文件越大。一般18到28区间选。-movflags +faststart:把MP4的元数据(moov)挪到文件头部。这对网页播放极其重要,因为浏览器需要先读到时长、尺寸等元数据才能显示进度条和首帧。如果不加这个参数,元数据在文件末尾,用户可能需要等整个文件下载完才能看到时长信息,体验一落千丈。-pix_fmt yuv420p:强制用YUV 4:2:0采样。许多剪辑原件是4:4:4或者带Alpha通道,这些格式浏览器硬解兼容性差,改成420能保证几乎所有设备都能硬解。-c:a aac -b:a 128k:音频用AAC,码率128kbps足够一般场景,说话类纯音频内容甚至可以降到96kbps。3.2 第二步:编写多源video标签转码完成后,正式在HTML里嵌视频。我平时使用的一个标准结构是这样的:<div class="video-wrap"> <video controls preload="metadata" poster="cover.jpg" playsinline> <source src="demo.mp4" type="video/mp4"> <source src="demo.webm" type="video/webm"> <p>当前浏览器不支持HTML5视频,建议更换到最新的主流浏览器。</p> </video> </div>注意几个细节:div.video-wrap不是必须的,但强烈建议加。视频组件以后几乎肯定要做响应式适配或加自定义控制层,有一个外层容器会让CSS定位简单很多。preload="metadata"让页面加载时不直接下载整个视频,又能拿到时长好渲染进度条。内部的中文降级文案,是给完全不支持video的老浏览器看的。现在的浏览器基本全军覆没支持,但诺基亚时代那种老产品上,至少能显示一句人话,不算白写。外层CSS按需设置,通用方案:.video-wrap { max-width: 100%; aspect-ratio: 16 / 9; } .video-wrap video { width: 100%; height: 100%; object-fit: cover; }aspect-ratio能保证视频区域在布局阶段就占好位置,避免页面加载时上下跳动。object-fit: cover是当视频尺寸和容器比例不一致时,裁剪边缘而不再拉伸画面。如果希望整个视频完整可见,不裁剪任何内容,用contain。3.3 第三步:完善降级方案和播放体验自动播放的背景视频是高频场景。比如某落地页的Hero区,希望视频自动循环播放,还要求它不出现在控制条里,代码长这样:<video autoplay loop muted playsinline preload="auto" poster="cover.jpg"> <source src="demo.mp4" type="video/mp4"> <source src="demo.webm" type="video/webm"> </video>muted在这里不是“没有声音”,而是为满足浏览器的自动播放策略。现实里有不少产品经理问“为什么加了autoplay没声音”,答案就是——浏览器认为未经用户手势就播放音频属于打扰,静音才能放行。如果你确实需要“页面加载后播放带声音的视频”,目前的正道是等用户点击页面任意位置后,用JavaScript调用video.play()并移除muted属性,而不是一上来就自动播。关于背景视频的常见误区,也是面试里不少人翻车的点:autoplay会忽略preload="none"的约束。因为浏览器需要立即加载数据才能播放,none在自动播放场景下是无效的,别看到网络面板里视频请求飞速加载就怀疑自己写错了。4. 视频不播放?常见问题排查实录4.1 常见问题速查表我把过去实际排查过的“视频不播放”案例汇总成下表,都是真实踩坑的高频项:症状最常见的根因解决办法点击播放后一直转圈,无画面视频编码浏览器不支持用ffmpeg转成H.264 + AAC有声音,画面黑屏视频像素格式不是YUV 4:2:0重新转码加-pix_fmt yuv420p控制条可用,但一直加载中服务端没有正确传Content-TypeNginx里补充MIME类型:video/mp4、video/webmiOS上视频自动全屏播放缺少playsinline属性video标签加playsinline自动播放无效视频带声音,被浏览器拦截加muted,必要时等用户交互后再设声音视频播放但很卡文件过大或码率过高转码压码率,或分清晰度多档源进度条拖不动MP4元数据在文件尾部转码加-movflags +faststart移动端流量下一直不加载preload="auto"在强下载大文件改成preload="metadata"这里重点说一下Nginx的MIME类型问题。很多人本地打开HTML一切正常,部署到服务器后视频一律404或者报unsupported source。本地之所以正常,是因为直接使用 file:// 协议放视频,浏览器不校验Content-Type;到了HTTP环境,服务器返回的Content-Type决定浏览器把它当“可播视频”还是“未知文件”。默认Nginx没有为mp4、webm配置对应的MIME,就会出现这问题。在Nginx配置里加:types { video/mp4 mp4; video/webm webm; video/quicktime mov; }就算你用的是其它Web服务器,也先确认响应头里的Content-Type正确,这往往是视频无法播放的隐形元凶。4.2 自动播放失效的真相自动播放策略是开发者提问率最高的话题。一个典型场景:在落地页放了一段产品宣传视频,希望首屏一进来就自动播,但肉眼观察就是不动。先说结论:在任何现代浏览器的默认设置下,带声音的自动播放一律被拦截。Chrome的自动播放策略分为三种情况:用户此前在该域名上表现出了“高度参与”(比如多次点击、播放过媒体),允许;用户点击了页面,允许;Muted视频,允许。第三种是最可控的,所以背景视频一律静音播放才是正道。Safari的策略更严。iOS Safari甚至不会让poster在设置preload的时候提前加载完整画面,它判定为“自动播放入口”才会真正加载数据。我在iPhone上调试背景视频时踩过的坑是,把playsinline漏掉了,结果每次进入页面视频都强制全屏——但那是旧版Safari行为,现在的Safari其实已经要求所有视频嵌入播放,playsinline依然是标配。有人会拿“为什么抖音网页版能自动播”来抬杠。原因就是它满足了两点:静音播放,并且在用户滑动屏幕这个手势事件之后才启动播放。你如果模仿这个套路,把autoplay从标签里去掉,改成监听整个窗口的touchstart、scroll事件再执行video.play(),就能绕开拦截,因为此时已经产生了用户交互。4.3 移动端兼容性细节移动端视频播放有两个让人抓狂的点:一个是样式不一致,另一个是性能瓶颈。样式上,iOS Safari会把video当作被自己接管的对象,你设置的圆角、阴影、object-fit在某些版本里会失效,甚至会出现两层视频画面(一层渲染在原生播放器层)。我的经验是:如果有复杂视觉需求,不要直接美化video元素,建议把视频渲染到canvas里作为二次处理。比如常见的“视频加高斯模糊背景”、“视频加扫描线滤镜”,都把帧数据画到Canvas再处理,可控性更强。性能上,移动端硬解视频会占用大量解码资源,一个1080p H.264视频的decode轨迹足以让中低端手机发热掉帧。如果目标是移动端为主的页面,我建议视频源尺寸压到720p甚至540p,码率控制在1Mbps以内,画质影响在手机小屏上几乎感知不到,但流畅度提升巨大。视频内容本身的帧率也可以从60fps降到30fps,省一半数据量,动画类内容几乎无感。5. 进阶技巧:封面、字幕与播放控制5.1 用poster做好首屏观感视频文件再轻也是按MB算的,页面首屏不能指望着立刻出画面。给video加poster是成本最低的体验优化。一个好的封面图要承载“这段视频讲什么”的信息,而不是简单截取第一帧,因为第一帧大概率是黑的或带色块的。poster属性写法:<video controls poster="cover.jpg" src="demo.mp4"></video>当用户开始播放时,poster会自动消失。如果视频是通过<source>加载的,同样生效。还有一个容易被无视的点:poster图会作为未播放态的画面参与版面布局,一旦用户播放后,视频实际尺寸或宽高比与封面不一致,页面会发生一次跳动。所以封面图尺寸比例一定要和视频本体一致,最好直接从视频的某一帧导出,再加工文字和图形。5.2 用track加字幕视频里有人声对白,建议加字幕,既方便听力障碍者,也方便公共场合静音现在越来越普遍。HTML5的原生方案是track标签配合WebVTT字幕文件。写一个简单的VTT文件subtitle.vtt:WEBVTT 00:00:00.000 --> 00:00:04.000 欢迎来到这个演示页面 00:00:04.000 --> 00:00:08.000 这一段介绍视频的基础用法页面里这样关联:<video controls> <source src="demo.mp4" type="video/mp4"> <track kind="subtitles" src="subtitle.vtt" srclang="zh-CN" label="中文" default> </video>注意kind的几个取值差别很大:subtitles是翻译字幕或补充说明,captions是包含环境音描速的字幕(比如电话铃声、门响),descriptions是给视觉障碍者听的文字描述,一般由屏幕阅读器读出。多数人用到的都是subtitles。长得像字幕的功能为什么总不生效?十次有八次是VTT文件的时间轴格式写错了,或者编码不是UTF-8。浏览器对VTT的解析要求非常严格,WEBVTT这个开头说明都不能少,时间戳之间必须用-->且两侧带空格。文件上传到服务器后,也要确认服务端返回的Content-Type是text/vtt。5.3 JavaScript自定义控制条自定义控制条是摆脱“浏览器原生长相”的唯一路径。逻辑不复杂,我就把骨架写下来。先把原生控制条关掉:<video id="player" preload="metadata" poster="cover.jpg"> <source src="demo.mp4" type="video/mp4"> </video> <div class="custom-controls"> <button id="playBtn">播放</button> <input id="progress" type="range" min="0" max="100" value="0"> <span id="time">00:00 / 00:00</span> <button id="fullscreenBtn">全屏</button> </div>对应JavaScript:const player = document.getElementById('player'); const playBtn = document.getElementById('playBtn'); const progress = document.getElementById('progress'); const time = document.getElementById('time'); playBtn.addEventListener('click', () => { if (player.paused) { player.play(); playBtn.textContent = '暂停'; } else { player.pause(); playBtn.textContent = '播放'; } }); player.addEventListener('timeupdate', () => { const percent = (player.currentTime / player.duration) * 100; progress.value = percent; time.textContent = formatTime(player.currentTime) + ' / ' + formatTime(player.duration); }); progress.addEventListener('input', () => { player.currentTime = (progress.value / 100) * player.duration; }); function formatTime(seconds) { if (isNaN(seconds)) return '00:00'; const m = Math.floor(seconds / 60); const s = Math.floor(seconds % 60); return String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0'); }这里有几条经验:timeupdate事件触发频率不高,VTT字幕解析、播放进度条更新它都够用,但如果你需要做帧级别的操作(比如逐帧比对),要用requestAnimationFrame循环读取currentTime。input事件拖动进度条时,别在每次触发都直接赋值给currentTime,那会频繁触发 seek 操作,体验卡顿,可以用change事件或者节流到几百毫秒一次。全屏也有讲究:直接调用player.requestFullscreen()会全屏整个页面包括控制条。只想全屏视频元素本身,可以给视频外面的容器加全屏,或调player.webkitEnterFullscreen()(iOS)。桌面端Chrome上requestFullscreen没有跨域限制,但移动端某些WebView里会被禁用,需要业务上兜底。Canvas方案也是一个方向:每隔一帧把video当前画面绘制到canvas上,再对Canvas做滤镜。这样能实现原生video完全不支持的旋转、马赛克、追色等视觉效果。代价是CPU/GPU占用显著上升,只适合短视频或画面较小的场景,做产品Demo很酷,做业务主力得掂量掂量性能预算。6. 从单页到规模化的经验心得6.1 视频压缩与性能预算视频文件体积过大,最大的受害者其实是首屏性能。我见过一个官网首屏里塞了14MB的MP4,用户打开页面光视频就下载了半天,转化率直线下降。所以视频上线之前,要给“体积预算”立规矩:背景视频或自动播放视频,建议控制在2MB以内,若超了就需要砍时长、降分辨率或换更强的编码。用户主动点击播放的教学视频、长视频,可以放宽,但视频页的首帧数据量要小,支持“边下边播”。一切视频都建议准备多清晰度:720p、480p、360p,根据网速切换。最朴素的做法是监听navigator.connection.downlink(单位是Mbps),小于1走360p,1到3走480p,高于3走720p。现实中极少有网站值得自己写一套视频CDN调度,但多清晰度切换不是多么高级的事,单纯一个<source>换文件路径就能做到。关键是别把逻辑绑在用户点播放那一刻,要在视频组件创建时就根据网络状态选定源。6.2 CDN与范围请求视频文件一旦超过几十MB,就不建议放在跟业务页面同一个静态目录了,原因有三:一是占应用服务器带宽,二是单机文件系统对大文件不友好,三是没法就近分发。正确做法是扔到对象存储加CDN服务上,把带签名的URL给video标签喂过去。CDN侧最容易被忽略的是“范围请求”(Range Request)。浏览器播放视频时,不是一次性把整个文件拖完,而是按需分段请求数据:先要前几KB的元数据,播放到中间再要中间那一段。服务器必须正确响应206 Partial Content,视频才能边下边播。对象存储默认支持,但你如果用自建服务器当源站,源站Nginx没开range相关配置,CDN就会拉不到分段数据,视频播放变成“下载完才播”,几十MB的文件卡到怀疑人生。6.3 大视频流媒体方案的取舍当业务从“网页上放一段视频”演进到“要做视频平台”,video标签就不能单兵作战了。要考虑HLS协议做自适应码率直播、DASH做更精细的码率分段,甚至要做加密视频(DRM)。HLS在PC端Chrome和Firefox不原生支持,需要引入hls.js这类第三方库来把HLS流包装成video可吃的blob数据。这个路子已经相当成熟,我在多个项目里这样实现过:视频地址给.m3u8,页面加载hls.js,根据浏览器能力自动决定是直接用原生HLS(Safari)还是转交给hls.js处理。DASH则是Google主导的标准,浏览器原生支持相对更好,但生态和工具链不如HLS成熟。普通团队完全没有必要在两种协议之间纠结,用一个现成播放器库(国产和开源都有成熟选择)就能全部处理。直接写video标签适合“单文件短视频”场景,一旦涉及地址列表、清晰度切换、加密和DRM,还是踏实用成熟播放器封装方案吧。有一点要分清:video标签永远是那个最终把画面渲染出来的容器,HLS也好DASH也好,不过是给它喂数据的“厨师”不同。不管你选哪条路,前面讲的标签属性、格式转码、poster和自动播放策略依然全部适用。做个网站视频,最优先要盯住的是“文件格式对不对、能不能自动播、有没有占位封面、服务端MIME配没配好”这四件事。把这四件事做扎实了,视频再怎么换包装、换播放器、换CDN,底层这摊子事都不会给你捣乱。我在实际项目里调过最多的就是MIME和自动播放策略这两个坑,希望这篇记录能帮你少走几趟弯路。
1. 先搞清楚:为什么视频不能直接塞进网页1.1 从GIF到HTML5视频的演进前几年还经常能看到有人用GIF做演示动效,一个十几秒的录屏转成GIF之后动辄几十MB,加载起来页面卡得不行。GIF本质上是图像格式,不是为连续画面临时设计的,位深只有8位,色彩断层严重,声音更是完全没戏。所以当你在HTML里需要播放一段真正带声音、色彩饱满的活动画面时,用GIF是“能用但极其痛苦”的兜底方案。HTML5标准在多年前就把<video>标签推进了规范,让浏览器原生支持视频播放,不需要任何插件。这个改变解决了三个痛点:一是文件体积比GIF小一个数量级,二是支持音频轨,三是播放控制能力(进度、声音、倍速)全部交给浏览器和JavaScript,开发者只负责给数据源和配置。这里要特别澄清一个误区:<video>标签并不是“把视频嵌入到网页里”,而是“让浏览器在文档中开辟一块独立区域,用它自己内核的解码器去拉取并渲染视频流”。这段视频始终是外部文件,HTML只是声明了它的位置和播放方式。1.2 视频格式与浏览器的“语言体系”为什么要先讲格式?因为HTML里的视频能不能播,代码写得再花哨都没用,浏览器不认识格式就摆一张裂开的图标。视频文件本身是一个容器,容器里装的是视频轨、音频轨和字幕轨,而视频轨又用不同的编码算法压缩。麻烦在于,不同浏览器支持的容器和编码组合不一样。迄今为止兼容性最好的组合是MP4容器配合H.264视频编码和AAC音频编码,几乎所有现代浏览器都能播。老牌企业级浏览器还对WebM(VP8/VP9编码)有一定支持,但覆盖率不如MP4。还有少部分场景会用到Ogg/Theora,基本被边缘化了。我给出的核心结论是:做网页视频,请无条件优先产出MP4/H.264/AAC格式;追求更高质量可以用H.265(HEVC),但Safari和部分移动端支持好,Windows上的Chrome和Firefox可能会直接拒绝播放,所以跨平台项目要谨慎。实操中我通常会额外准备一份WebM版本,专门喂给Chrome和Firefox,既能用VP9把画质做高,体积还比H.264更小。提示:视频格式不是看扩展名,而是看容器内的编码信息。你完全可以拿到一个后缀为.mp4的文件,但里面视频轨是HEVC,结果在某个浏览器上死活播放不了。2. 揭晓主角:video标签的完整骨架2.1 最简用法:一行代码真的能播视频如果你手里已经有一个MP4文件,路径正确,网络正常,那么下面这段代码确实能跑:<video src="./demo.mp4" controls></video>controls属性决定浏览器是否显示原生控制条,包括播放/暂停按钮、进度条、音量、全屏入口。不加它,视频区就是一块“哑巴画面”,用户点哪儿都没反应,只能通过JavaScript触发播放。如果连src都没有,那video标签就变成一块空区域,高度塌陷成0,在页面上什么也看不到。实操层面每个人都要养成习惯:写上controls只是怒下限,不要把原生控制条当成最终UI。尤其面向C端产品时,原生控制条在不同平台上的观感差异极大(Windows上Chrome的样式和iOS Safari的样式完全不同),真要打磨体验,后面第5节会用JavaScript做自定义控制层。2.2 单标签替代方案:source多源设置src属性直给一个文件是简单场景。真正的线上项目里,为了兼顾画质和兼容性,我会放多个格式的源文件。这时就不能再用src属性,而是把video标签写空,里面逐个塞<source>子标签:<video controls> <source src="demo.mp4" type="video/mp4"> <source src="demo.webm" type="video/webm"> </video>浏览器拿到video标签后,会从上到下依次检查source的type是否在自己支持列表里。如果支持就拿它播,不支持就跳过看下一个。若所有source都不支持,整个视频区域显示空白,并且触发video的 error 事件。这里有个隐藏好处:type属性不仅告诉浏览器“我是什么格式”,还能让它第一时间跳过完全不可能支持的源,减少无效请求。你不写type它也是能工作的,浏览器会下载文件头部一小段,嗅探真实格式再决定;但写了更规范,也能省掉不必要的内容协商。2.3 关键属性逐个掰开揉碎autoplay:页面加载后自动开始播放。但这条属性在大多数浏览器里默认无效,因为浏览器禁止带声音的视频自动播放,防止打开一堆页面后背景里突然有人说话吓人一跳。解决办法是同时加muted属性(静音播放),并且最好再加上playsinline(iOS Safari上不允许视频自动全屏播放必备)。loop:循环播放。做背景视频、免打扰动效时配合muted+autoplay使用。poster:视频加载完或播放前的占位封面图。不设置就是第一帧画面,有些情况第一帧是黑屏或杂乱画面,体验很差,建议给一张有设计感的海报图。preload:告诉浏览器要不要预加载视频数据。可选none、metadata、auto。none就是不预下载,等到用户点播放再请求;metadata只下载元数据(时长、尺寸、首帧);auto尽量预加载整个文件。移动端流量敏感,建议默认metadata或none。width/height:设置视频区域宽高。建议只定其中一个,另一个由浏览器按视频原始宽高比自动计算;两个都定但比例不一致时,视频会变形拉伸,画面里的人会变成矮胖版或瘦长版。crossorigin:当你的视频用来做Canvas离屏处理、或者从跨域CDN拉取时,需要设置它才能让浏览器认为“这是一个合法的跨域资源”。普通播放不需要加。disablepictureinpicture:关闭画中画按钮。不是所有浏览器都认,但新版Chrome支持良好。如果你不希望用户把视频拖成小窗一直霸占屏幕,加上它。controlslist:精细控制原生控制条上显示哪些按钮,比如不显示下载按钮、不显示全屏按钮。它是不完全标准的属性,在Chrome系有效,Safari就不完全响应。3. 实操:从零到一在页面里播放视频3.1 第一步:准备素材,确定编码视频源从哪里来?可能是剪辑软件导出的成片、手机拍的竖屏短视频、也可能是服务端自动转码后给出的流地址。不管来源,只要最终推到网页上的成品,我都建议过一遍转码流程。转码工具我用的是ffmpeg命令行,只要电脑装了ffmpeg,就可以固定用下面这套参数生产Web兼容性最好的MP4:ffmpeg -i input.mov -c:v libx264 -profile:v high -level 4.0 \ -crf 23 -preset medium -c:a aac -b:a 128k \ -movflags +faststart -pix_fmt yuv420p output.mp4逐项解释这些参数,因为每一个都踩过坑:-profile:v high -level 4.0:H.264编码规范里的档位和级别。high档画质好,4.0对应最大分辨率到1080p级别,能覆盖绝大多数浏览器硬解需求。不要用baseline,那是旧设备级别,画质损失明显。-crf 23:恒定质量参数,23是某位平衡值,数字越小画质越好文件越大。一般18到28区间选。-movflags +faststart:把MP4的元数据(moov)挪到文件头部。这对网页播放极其重要,因为浏览器需要先读到时长、尺寸等元数据才能显示进度条和首帧。如果不加这个参数,元数据在文件末尾,用户可能需要等整个文件下载完才能看到时长信息,体验一落千丈。-pix_fmt yuv420p:强制用YUV 4:2:0采样。许多剪辑原件是4:4:4或者带Alpha通道,这些格式浏览器硬解兼容性差,改成420能保证几乎所有设备都能硬解。-c:a aac -b:a 128k:音频用AAC,码率128kbps足够一般场景,说话类纯音频内容甚至可以降到96kbps。3.2 第二步:编写多源video标签转码完成后,正式在HTML里嵌视频。我平时使用的一个标准结构是这样的:<div class="video-wrap"> <video controls preload="metadata" poster="cover.jpg" playsinline> <source src="demo.mp4" type="video/mp4"> <source src="demo.webm" type="video/webm"> <p>当前浏览器不支持HTML5视频,建议更换到最新的主流浏览器。</p> </video> </div>注意几个细节:div.video-wrap不是必须的,但强烈建议加。视频组件以后几乎肯定要做响应式适配或加自定义控制层,有一个外层容器会让CSS定位简单很多。preload="metadata"让页面加载时不直接下载整个视频,又能拿到时长好渲染进度条。内部的中文降级文案,是给完全不支持video的老浏览器看的。现在的浏览器基本全军覆没支持,但诺基亚时代那种老产品上,至少能显示一句人话,不算白写。外层CSS按需设置,通用方案:.video-wrap { max-width: 100%; aspect-ratio: 16 / 9; } .video-wrap video { width: 100%; height: 100%; object-fit: cover; }aspect-ratio能保证视频区域在布局阶段就占好位置,避免页面加载时上下跳动。object-fit: cover是当视频尺寸和容器比例不一致时,裁剪边缘而不再拉伸画面。如果希望整个视频完整可见,不裁剪任何内容,用contain。3.3 第三步:完善降级方案和播放体验自动播放的背景视频是高频场景。比如某落地页的Hero区,希望视频自动循环播放,还要求它不出现在控制条里,代码长这样:<video autoplay loop muted playsinline preload="auto" poster="cover.jpg"> <source src="demo.mp4" type="video/mp4"> <source src="demo.webm" type="video/webm"> </video>muted在这里不是“没有声音”,而是为满足浏览器的自动播放策略。现实里有不少产品经理问“为什么加了autoplay没声音”,答案就是——浏览器认为未经用户手势就播放音频属于打扰,静音才能放行。如果你确实需要“页面加载后播放带声音的视频”,目前的正道是等用户点击页面任意位置后,用JavaScript调用video.play()并移除muted属性,而不是一上来就自动播。关于背景视频的常见误区,也是面试里不少人翻车的点:autoplay会忽略preload="none"的约束。因为浏览器需要立即加载数据才能播放,none在自动播放场景下是无效的,别看到网络面板里视频请求飞速加载就怀疑自己写错了。4. 视频不播放?常见问题排查实录4.1 常见问题速查表我把过去实际排查过的“视频不播放”案例汇总成下表,都是真实踩坑的高频项:症状最常见的根因解决办法点击播放后一直转圈,无画面视频编码浏览器不支持用ffmpeg转成H.264 + AAC有声音,画面黑屏视频像素格式不是YUV 4:2:0重新转码加-pix_fmt yuv420p控制条可用,但一直加载中服务端没有正确传Content-TypeNginx里补充MIME类型:video/mp4、video/webmiOS上视频自动全屏播放缺少playsinline属性video标签加playsinline自动播放无效视频带声音,被浏览器拦截加muted,必要时等用户交互后再设声音视频播放但很卡文件过大或码率过高转码压码率,或分清晰度多档源进度条拖不动MP4元数据在文件尾部转码加-movflags +faststart移动端流量下一直不加载preload="auto"在强下载大文件改成preload="metadata"这里重点说一下Nginx的MIME类型问题。很多人本地打开HTML一切正常,部署到服务器后视频一律404或者报unsupported source。本地之所以正常,是因为直接使用 file:// 协议放视频,浏览器不校验Content-Type;到了HTTP环境,服务器返回的Content-Type决定浏览器把它当“可播视频”还是“未知文件”。默认Nginx没有为mp4、webm配置对应的MIME,就会出现这问题。在Nginx配置里加:types { video/mp4 mp4; video/webm webm; video/quicktime mov; }就算你用的是其它Web服务器,也先确认响应头里的Content-Type正确,这往往是视频无法播放的隐形元凶。4.2 自动播放失效的真相自动播放策略是开发者提问率最高的话题。一个典型场景:在落地页放了一段产品宣传视频,希望首屏一进来就自动播,但肉眼观察就是不动。先说结论:在任何现代浏览器的默认设置下,带声音的自动播放一律被拦截。Chrome的自动播放策略分为三种情况:用户此前在该域名上表现出了“高度参与”(比如多次点击、播放过媒体),允许;用户点击了页面,允许;Muted视频,允许。第三种是最可控的,所以背景视频一律静音播放才是正道。Safari的策略更严。iOS Safari甚至不会让poster在设置preload的时候提前加载完整画面,它判定为“自动播放入口”才会真正加载数据。我在iPhone上调试背景视频时踩过的坑是,把playsinline漏掉了,结果每次进入页面视频都强制全屏——但那是旧版Safari行为,现在的Safari其实已经要求所有视频嵌入播放,playsinline依然是标配。有人会拿“为什么抖音网页版能自动播”来抬杠。原因就是它满足了两点:静音播放,并且在用户滑动屏幕这个手势事件之后才启动播放。你如果模仿这个套路,把autoplay从标签里去掉,改成监听整个窗口的touchstart、scroll事件再执行video.play(),就能绕开拦截,因为此时已经产生了用户交互。4.3 移动端兼容性细节移动端视频播放有两个让人抓狂的点:一个是样式不一致,另一个是性能瓶颈。样式上,iOS Safari会把video当作被自己接管的对象,你设置的圆角、阴影、object-fit在某些版本里会失效,甚至会出现两层视频画面(一层渲染在原生播放器层)。我的经验是:如果有复杂视觉需求,不要直接美化video元素,建议把视频渲染到canvas里作为二次处理。比如常见的“视频加高斯模糊背景”、“视频加扫描线滤镜”,都把帧数据画到Canvas再处理,可控性更强。性能上,移动端硬解视频会占用大量解码资源,一个1080p H.264视频的decode轨迹足以让中低端手机发热掉帧。如果目标是移动端为主的页面,我建议视频源尺寸压到720p甚至540p,码率控制在1Mbps以内,画质影响在手机小屏上几乎感知不到,但流畅度提升巨大。视频内容本身的帧率也可以从60fps降到30fps,省一半数据量,动画类内容几乎无感。5. 进阶技巧:封面、字幕与播放控制5.1 用poster做好首屏观感视频文件再轻也是按MB算的,页面首屏不能指望着立刻出画面。给video加poster是成本最低的体验优化。一个好的封面图要承载“这段视频讲什么”的信息,而不是简单截取第一帧,因为第一帧大概率是黑的或带色块的。poster属性写法:<video controls poster="cover.jpg" src="demo.mp4"></video>当用户开始播放时,poster会自动消失。如果视频是通过<source>加载的,同样生效。还有一个容易被无视的点:poster图会作为未播放态的画面参与版面布局,一旦用户播放后,视频实际尺寸或宽高比与封面不一致,页面会发生一次跳动。所以封面图尺寸比例一定要和视频本体一致,最好直接从视频的某一帧导出,再加工文字和图形。5.2 用track加字幕视频里有人声对白,建议加字幕,既方便听力障碍者,也方便公共场合静音现在越来越普遍。HTML5的原生方案是track标签配合WebVTT字幕文件。写一个简单的VTT文件subtitle.vtt:WEBVTT 00:00:00.000 --> 00:00:04.000 欢迎来到这个演示页面 00:00:04.000 --> 00:00:08.000 这一段介绍视频的基础用法页面里这样关联:<video controls> <source src="demo.mp4" type="video/mp4"> <track kind="subtitles" src="subtitle.vtt" srclang="zh-CN" label="中文" default> </video>注意kind的几个取值差别很大:subtitles是翻译字幕或补充说明,captions是包含环境音描速的字幕(比如电话铃声、门响),descriptions是给视觉障碍者听的文字描述,一般由屏幕阅读器读出。多数人用到的都是subtitles。长得像字幕的功能为什么总不生效?十次有八次是VTT文件的时间轴格式写错了,或者编码不是UTF-8。浏览器对VTT的解析要求非常严格,WEBVTT这个开头说明都不能少,时间戳之间必须用-->且两侧带空格。文件上传到服务器后,也要确认服务端返回的Content-Type是text/vtt。5.3 JavaScript自定义控制条自定义控制条是摆脱“浏览器原生长相”的唯一路径。逻辑不复杂,我就把骨架写下来。先把原生控制条关掉:<video id="player" preload="metadata" poster="cover.jpg"> <source src="demo.mp4" type="video/mp4"> </video> <div class="custom-controls"> <button id="playBtn">播放</button> <input id="progress" type="range" min="0" max="100" value="0"> <span id="time">00:00 / 00:00</span> <button id="fullscreenBtn">全屏</button> </div>对应JavaScript:const player = document.getElementById('player'); const playBtn = document.getElementById('playBtn'); const progress = document.getElementById('progress'); const time = document.getElementById('time'); playBtn.addEventListener('click', () => { if (player.paused) { player.play(); playBtn.textContent = '暂停'; } else { player.pause(); playBtn.textContent = '播放'; } }); player.addEventListener('timeupdate', () => { const percent = (player.currentTime / player.duration) * 100; progress.value = percent; time.textContent = formatTime(player.currentTime) + ' / ' + formatTime(player.duration); }); progress.addEventListener('input', () => { player.currentTime = (progress.value / 100) * player.duration; }); function formatTime(seconds) { if (isNaN(seconds)) return '00:00'; const m = Math.floor(seconds / 60); const s = Math.floor(seconds % 60); return String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0'); }这里有几条经验:timeupdate事件触发频率不高,VTT字幕解析、播放进度条更新它都够用,但如果你需要做帧级别的操作(比如逐帧比对),要用requestAnimationFrame循环读取currentTime。input事件拖动进度条时,别在每次触发都直接赋值给currentTime,那会频繁触发 seek 操作,体验卡顿,可以用change事件或者节流到几百毫秒一次。全屏也有讲究:直接调用player.requestFullscreen()会全屏整个页面包括控制条。只想全屏视频元素本身,可以给视频外面的容器加全屏,或调player.webkitEnterFullscreen()(iOS)。桌面端Chrome上requestFullscreen没有跨域限制,但移动端某些WebView里会被禁用,需要业务上兜底。Canvas方案也是一个方向:每隔一帧把video当前画面绘制到canvas上,再对Canvas做滤镜。这样能实现原生video完全不支持的旋转、马赛克、追色等视觉效果。代价是CPU/GPU占用显著上升,只适合短视频或画面较小的场景,做产品Demo很酷,做业务主力得掂量掂量性能预算。6. 从单页到规模化的经验心得6.1 视频压缩与性能预算视频文件体积过大,最大的受害者其实是首屏性能。我见过一个官网首屏里塞了14MB的MP4,用户打开页面光视频就下载了半天,转化率直线下降。所以视频上线之前,要给“体积预算”立规矩:背景视频或自动播放视频,建议控制在2MB以内,若超了就需要砍时长、降分辨率或换更强的编码。用户主动点击播放的教学视频、长视频,可以放宽,但视频页的首帧数据量要小,支持“边下边播”。一切视频都建议准备多清晰度:720p、480p、360p,根据网速切换。最朴素的做法是监听navigator.connection.downlink(单位是Mbps),小于1走360p,1到3走480p,高于3走720p。现实中极少有网站值得自己写一套视频CDN调度,但多清晰度切换不是多么高级的事,单纯一个<source>换文件路径就能做到。关键是别把逻辑绑在用户点播放那一刻,要在视频组件创建时就根据网络状态选定源。6.2 CDN与范围请求视频文件一旦超过几十MB,就不建议放在跟业务页面同一个静态目录了,原因有三:一是占应用服务器带宽,二是单机文件系统对大文件不友好,三是没法就近分发。正确做法是扔到对象存储加CDN服务上,把带签名的URL给video标签喂过去。CDN侧最容易被忽略的是“范围请求”(Range Request)。浏览器播放视频时,不是一次性把整个文件拖完,而是按需分段请求数据:先要前几KB的元数据,播放到中间再要中间那一段。服务器必须正确响应206 Partial Content,视频才能边下边播。对象存储默认支持,但你如果用自建服务器当源站,源站Nginx没开range相关配置,CDN就会拉不到分段数据,视频播放变成“下载完才播”,几十MB的文件卡到怀疑人生。6.3 大视频流媒体方案的取舍当业务从“网页上放一段视频”演进到“要做视频平台”,video标签就不能单兵作战了。要考虑HLS协议做自适应码率直播、DASH做更精细的码率分段,甚至要做加密视频(DRM)。HLS在PC端Chrome和Firefox不原生支持,需要引入hls.js这类第三方库来把HLS流包装成video可吃的blob数据。这个路子已经相当成熟,我在多个项目里这样实现过:视频地址给.m3u8,页面加载hls.js,根据浏览器能力自动决定是直接用原生HLS(Safari)还是转交给hls.js处理。DASH则是Google主导的标准,浏览器原生支持相对更好,但生态和工具链不如HLS成熟。普通团队完全没有必要在两种协议之间纠结,用一个现成播放器库(国产和开源都有成熟选择)就能全部处理。直接写video标签适合“单文件短视频”场景,一旦涉及地址列表、清晰度切换、加密和DRM,还是踏实用成熟播放器封装方案吧。有一点要分清:video标签永远是那个最终把画面渲染出来的容器,HLS也好DASH也好,不过是给它喂数据的“厨师”不同。不管你选哪条路,前面讲的标签属性、格式转码、poster和自动播放策略依然全部适用。做个网站视频,最优先要盯住的是“文件格式对不对、能不能自动播、有没有占位封面、服务端MIME配没配好”这四件事。把这四件事做扎实了,视频再怎么换包装、换播放器、换CDN,底层这摊子事都不会给你捣乱。我在实际项目里调过最多的就是MIME和自动播放策略这两个坑,希望这篇记录能帮你少走几趟弯路。