html探索系列-video标签使用与兼容
<video> 用来在 HTML 文档里嵌入媒体播放器,在页面内播视频。它也能播音频,但音频更适合用 <audio>。
标准属性在桌面大体够用;移动端还要面对内联播放、自动播放策略,以及微信、QQ 里的 X5 内核。下面按属性、事件、策略和踩坑把这些能力串起来。
下面是一个局域播放示例:视频在文档里播,上方可以叠 HTML。封面、片源、字幕路径与 Demo 一致。
<div class="player_box">
<video
controls
controlslist="nodownload nofullscreen noremoteplayback"
loop="true"
poster="https://my-files-1259410276.cos.ap-chengdu.myqcloud.com/md_images/馒小蛮同学-天也不懂情.png"
x5-playsinline="true"
x5-video-player-type="h5-page"
webkit-playsinline="true"
playsinline="true"
currentTime="100"
muted
>
<source src="https://my-files-1259410276.cos.ap-chengdu.myqcloud.com/media/美人谷.mp4" type="video/mp4">
<p>
Your browser doesn't support HTML5 video. Here is a <a href="myVideo.mp4">link to the video</a> instead.
</p>
<track default kind="subtitles" src="美人谷 - 阿兰.vtt" srclang="cn" label="中文字幕">
<track kind="subtitles" src="sampleSubtitles_de.vtt" srclang="de">
<track kind="descriptions" src="sampleDescriptions.vtt" srclang="en">
<track kind="chapters" src="sampleChapters.vtt" srclang="en">
<track kind="metadata" src="keyStage3.vtt" srclang="en" label="Key Stage 3">
</video>
<div class="player_pop"></div>
</div>
<video> 标签里的内容是浏览器不支持该元素时的降级。浏览器支持的编码并不相同,可以在 <source> 里提供多个源,浏览器使用它能播的第一个。
<track> 用来挂时序文本(字幕或按时间对齐的数据)。
其他注意:
- 不写
controls,浏览器就不画自带控件。可以用 JavaScript 和 HTMLMediaElement API 自己做控件。 - HTMLMediaElement 会派发多种事件,用来控制视频(和音频)。
- 用
object-position调整画面在盒子里的位置;用object-fit控制画面如何适应外框。 - 字幕、标题可以基于
<track>和 WebVTT,再用 JavaScript 控制。
video 的属性
autoplay
布尔属性。指定后视频会马上自动播放,不必再调 play()。自动播放受浏览器政策限制,各浏览器表现不同。移动端 App 内嵌 WebView 往往另有参数控制是否允许自动播放。
controls
加上后,浏览器提供控件,用户可以调音量、拖进度、暂停和恢复。不只是 Gecko,现行浏览器都如此。
controlslist
浏览器显示自带控件集时(例如写了 controls),用它提示要隐藏哪些控件。可取值:nodownload、nofullscreen、noremoteplayback。
<video controls controlslist="nodownload nofullscreen noremoteplayback" src=""></video>
currentTime
JavaScript 属性,不是可靠的 HTML 属性。读取返回当前播放位置(秒)。尚未开始播放时,设置的是开始后的偏移。赋值会在媒体加载后跳到该时间。
媒体正在播时,若缓冲区数据已经过期,用户代理有可能拿不到数据。有些媒体的时间线不以 0 秒开头,应把 currentTime 设在有效区间内。getStartDate() 可用来确定媒体时间线的起始。
document.querySelector('video').currentTime = 100
实测(lizhao,2020.06.07):
- 直接在
<video>上写currentTime有兼容问题,Chrome、安卓微信等会无效。 - 只有视频开始播放后设置的
currentTime才有效。 - 不能在
canplay里设:会陷入死循环。
loop
布尔属性,播完自动从头再来。HTML 里写成 loop 即可,不必 loop="true"。
muted
布尔属性,音频默认静音。默认 false,即画面响时声音也响。
document.querySelector('video').muted = true
直接在标签上写 muted 仍有兼容问题,见后文。
playsinline
布尔属性,标记在文档内播放、不脱离文档流。没有它并不等于「永远全屏」。
playsinline是标准写法。webkit-playsinline是 iOS 10 之前的写法,前缀在 iOS 10 里去掉了。当时 iOS 微信仍需要带前缀的写法。x5-playsinline是 X5 内核写法(微信、QQ 浏览器等腾讯产品)。
<video
x5-playsinline="true"
webkit-playsinline="true"
playsinline="true"
x-webkit-airplay="true"
x5-video-player-type="h5"
x5-video-player-fullscreen=""
x5-video-orientation="portrait"
poster=""
src=""
></video>
<!-- X5 内核里不自动全屏:设 x5-playsinline -->
<!-- 安卓 X5 不要同时加 x-webkit-airplay="allow"、x5-video-player-fullscreen="true"(加了之后 playsinline 会失效) -->
playsinline 还要求宿主 App 打开内联播放,例如 allowsInlineMediaPlayback = YES。App 不设,页面写了也无效。这就是不少安卓微信里视频总是全屏的原因:App 侧不支持内联;iOS 微信则支持。
在 iOS 10 Safari 里,<video playsinline> 可以内联播放:有该属性时不会自动全屏,用户仍可点全屏按钮;没有则播放时自动全屏。无论是否设置,退出全屏后视频都会继续播。
若要做全屏直播或全屏 H5:iOS 需要去掉 webkit-playsinline(写成 false 无效),安卓默认全屏则不必。此时全屏仍带播放控件,与有没有 controls 无关。
poster
海报帧 URL,在用户播放或跳帧之前展示。未指定时,第一帧出来之前是空白,之后第一帧当海报。不同环境有兼容问题,见后文。
preload
提示浏览器怎样预加载最合适。规范不强制遵守,只是提示。
none:用户多半不会看,尽量少占流量,不必缓存。metadata:用户未必看,但拉时长等元数据仍合理。auto:优先加载,必要时可下完整文件。- 空字符串:等同
auto。 - 不写:由浏览器自己定。规范建议默认
metadata。
autoplay 优先于 preload。用户要自动播,浏览器就必须下载。两者同时写,规范允许。
src
要嵌入的视频 URL。也可以不用它,改在 <video> 里写 <source>。
width 和 height
width、height 是视频展示区域的宽高,单位是 CSS 像素。
x5-video-player-type
启用同层 H5 播放器。可选值 h5-page。视频全屏时(即不加 x5-playsinline="true"),页面元素可以叠在视频层上。安卓微信特有。
x5-video-orientation
声明播放器支持的方向。landscape 横屏,portrait 竖屏(有的示例写成 portraint,那是拼写错误)。默认竖屏。只在声明了 x5-video-player-type="h5" 时生效。
<video x5-video-player-type="h5" x5-video-orientation="landscape"></video> <!-- 横屏 -->
<video x5-video-player-type="h5" x5-video-orientation="portrait"></video> <!-- 竖屏 -->
<video x5-video-player-type="h5" x5-video-orientation="landscape|portrait"></video> <!-- 跟随手机旋转 -->
x5-video-player-fullscreen
播放时进入全屏。true 支持,false 不支持,默认 false。不声明时,页面视口仍是播放前的原始大小。例如微信常驻标题栏高度不会给页面,播放时容易上下各一块黑边。声明后需要自己适配新视口,可监听 resize。
其他属性
- autobuffer:旧属性,现行标准已去掉,改用
preload。原先表示即使不自动播放也开始缓存,适合「打开就是播视频」的页面,一直缓存到媒体缓存满。 - crossorigin:枚举属性,说明抓取视频是否走 CORS。走 CORS 且服务器配合的资源,可以画进
<canvas>而不被污染。anonymous:发跨域请求(带Origin头),不带凭证(Cookie、证书、HTTP Basic)。服务器若不返回Access-Control-Allow-Origin,资源会被污染,使用受限。use-credentials:发跨域请求并带凭证。服务器若不返回Access-Control-Allow-Credentials,同样会被污染。- 不写该属性:不走 CORS(不发
Origin),在<video>里播一般不会污染。非法值按anonymous处理。
- disablePictureInPicture:阻止浏览器建议画中画,或在某些情况下自动进入画中画。
- disableRemotePlayback:布尔属性,禁用有线(HDMI、DVI 等)和无线(Miracast、Chromecast、DLNA、AirPlay 等)远程播放。
duration(只读):媒体总时长(秒)。没有媒体或媒体无效时为
NaN。没有已知终点(直播、网播、WebRTC 等)时为Infinity。document.querySelector('video').durationintrinsicsize:实验属性,用来覆盖媒体元素的固有尺寸。图像会按这些尺寸光栅化,
naturalWidth、naturalHeight返回该值;视频按此大小光栅化,但videoWidth、videoHeight仍返回真实固有尺寸。需要打开 Chrome 的 Experimental Web Platform features(chrome://flags/)。未进入现行标准;Chrome 实测无效(lizhao,2020/05/29)。played:返回
TimeRanges。表示用户已经播过或看过的时间段。若用户在视频里跳跃,会有多段。document.querySelector('video').played.length // 已播放的范围个数 document.querySelector('video').played.start(0) // 第一段开始时间 document.querySelector('video').played.end(0) // 第一段结束时间readyState:媒体就绪状态。
| 常量 | 值 | 含义 | | --- | --- | --- | |
HAVE_NOTHING| 0 | 还没有任何就绪信息 | |HAVE_METADATA| 1 | 已初始化,元数据可用 | |HAVE_CURRENT_DATA| 2 | 当前位置可播,但还不够播下一帧 | |HAVE_FUTURE_DATA| 3 | 当前及至少下一帧可用 | |HAVE_ENOUGH_DATA| 4 | 数据足以开始播放;网速稳住的话可以播完 |- seeking:用户是否正在寻址(拖到新位置)。
- volume:音量,0-1。
- x-webkit-airplay:iOS WebKit 属性,让视频支持 AirPlay,投到音箱、Apple TV 等设备。
video 的方法
addTextTrack
向音频、视频添加文本轨道。现行主流浏览器已支持。字幕来自文件时,更常用 <track>。
document.querySelector('video').addTextTrack(kind, label, language)
canPlayType
检测浏览器能否播放指定类型,返回字符串:probably(很有可能)、maybe(也许)、""(不支持)。
document.querySelector('video').canPlayType('video/mp4')
load
重新加载媒体元素。
document.querySelector('video').load()
play
开始播放。现行实现返回 Promise,可用来探测自动播放是否被拦。
document.querySelector('video').play()
pause
暂停。
document.querySelector('video').pause()
video 的事件
canplay
已有足够数据(至少数帧)可供播放。
canplaythrough
按当前下载速度可以不被中断地播完。手动设置 currentTime 会让 Firefox 再触发一次,其他浏览器不一定。
ended
播放结束。
error
出错。细节看元素的 error 属性,见 HTMLMediaElement.error。
loadedmetadata
元数据已加载,相关属性有了有效值。
loadeddata
第一帧已加载。
play
从暂停恢复播放。
playing
开始出画面(初次、暂停后恢复、结束后重来都算)。
pause
暂停。
progress
周期性报告下载进度。已下载范围看 buffered。
timeupdate
currentTime 变了。
waiting
待执行的操作(如回放)在等另一个操作(如跳跃、下载)。
其他事件
- abort:播放被终止时触发。例如播放中的视频重新开始。
- durationchange:媒体长度改变。例如已经加载到足以得知总时长。
- emptied:媒体被清空(初始化)。
- loadstart:开始加载。
- mozaudioavailable:Firefox 专用,音频数据已缓存并交给音频层。
- ratechange:回放速率变化。
- seeked:跳跃完成。
- seeking:跳跃开始。
- stalled:尝试取数据但不可用。
- suspend:加载终止(下完了,或因故暂停)。
- volumechange:音量变了(
volume或muted)。
source
<source> 给 <picture>、<audio>、<video> 指定多个媒体资源。空元素。常用来按浏览器支持的格式提供同一内容。
track
<track> 作为 <audio>、<video> 的子元素,挂时序文本。格式主要是 WebVTT(.vtt),也可以是 TTML。
default:除非用户偏好指定了更合适的轨道,否则启用这条。每个媒体元素上只能有一个 track 带该属性。
kind(省略时默认 subtitles):
- subtitles:给看不懂的对白做翻译,也可带场景时间地点等背景。
- captions:对白乃至音效的转录或翻译,给听障或静音时用。
- descriptions:画面的文本描述,给视障或看不见视频时用。
- chapters:章节标题,方便浏览。
- metadata:给脚本用,对用户不可见。
label:列出可用轨道时,给用户看的标题。
src:轨道地址,必须是合法 URL,且必须写。
srclang:文本语言,须是合法 BCP 47 标签。kind 为 subtitles 时必须写。示例:zh 中文、zh-cmn 普通话、zh-Hans 简体、zh-yue 粤语、zh-Hant-HK 香港繁体、en 英语、en-GB 英式英语。Demo 里写的 srclang="cn" 不是规范标签,规范应用 zh。
同一媒体元素上,任意两个 track 不能同时有相同的 kind、srclang 和 label。
<track> 是 HTML5 新元素,接入前要核兼容性。
WebVTT
Web 视频文本轨格式(WebVTT) 配合 <track> 显示定时字幕。文件必须用 UTF-8。空格处也可用制表符。MIME 类型 text/vtt。
WEBVTT 美人谷
00:02.000 --> 00:05.000
美人谷 - 阿兰
00:07.950 --> 00:15.760
词:毛慧
曲:阿兰/毛慧
编曲:叶月/王晨
制作人:毛慧
录音:张生磊(记忆时刻录音棚)
00:38.130 --> 00:44.180
越过山河的神秘
幕自然的洗礼
看见山川出云泽被着大地
可以用 CSS 改字幕样式:
video::cue {
background-image: linear-gradient(to bottom, dimgray, lightgray);
color: papayawhip;
}
video::cue(b) {
color: peachpuff;
}
也可以写在 .vtt 里:
STYLE
::cue {
background-image: linear-gradient(to bottom, dimgray, lightgray);
color: papayawhip;
}
00:38.130 --> 00:44.180
越过山河的神秘
幕自然的洗礼
看见山川出云泽被着大地
自动播放
早期安卓和 iOS 都要用户手势才能自动播。后来策略放宽,但各机型、各内置浏览器仍不一致。
探测当前环境能否自动播(play() 返回 Promise 时):
let promise = document.querySelector('video').play();
if (promise !== undefined) {
promise.then(() => {
// video can play
}).catch(err => {
// video cannot play
})
}
App 内嵌 WebView(如 WKWebView)另有参数控制自动播放。例如钉钉允许、微信禁止,以宿主为准。
微信可用内置事件辅助自动播(须加载 JSSDK,如 jweixin-1.2.0.js)。实测:iPhone 11 Pro 微信可以自动播;小米 10 微信不行(lizhao,2020/06/07)。
document.addEventListener("WeixinJSBridgeReady", function () {
document.querySelector('video').play();
}, false)
Chrome 自动播放
实测(lizhao,2020/05/30):加上 muted 后可以自动播放。现行 Chrome 对有声自动播仍限制,静音或用户曾与站点有媒体互动时更容易过。
Android 自动播放
小米 10 实测(lizhao,2020/05/30):UC、微信不支持自动播(有没有 muted 一样),标签写 autoplay 和 JS 调 play() 都无效。QQ 浏览器支持自动播(与 muted 无关)。
iOS 自动播放政策
iOS 8:允许 preload="metadata" 预加载元数据,但 <video> 不能自动播、不能内联,只能在用户主动操作后播放,且必须全屏。
「用户主动操作」包括:点视频播放按钮;在 touchend、click、doubleclick、keydown 的处理函数里直接调 video.play()。button.addEventListener('click', () => { video.play(); }) 满足;video.addEventListener('canplaythrough', () => { video.play(); }) 不满足。
以上是 Safari。iOS App 给 WebView 设 mediaPlaybackRequiresUserAction 和 allowsInlineMediaPlayback 后,页面可以用 autoplay 和 webkit-playsinline 做自动播和内联。
iOS 10:Safari 允许自动播两类视频:无音轨;设了 muted 的无声视频。可用 <video autoplay> 或 video.play(),不必用户手势。播放中一旦有了声音(有了音轨或取消 muted),Safari 会暂停。用 <video autoplay> 的元素还须在可视区内,滚出视口也会暂停。
全屏与局域播放
- 全屏播放:一般调系统 native 播放器,盖在 WebView 之上,控件无法隐藏。安卓和 iOS 在微信里默认都是全屏。
- 局域播放:在文档内播,视频上方可以叠 HTML。不写
controls可以藏控制条(实测:微信内置浏览器、QQ 浏览器、UC;lizhao,2020/05/30)。
iOS:默认全屏。局域播放设 webkit-playsinline="true"。
Android:微信默认全屏;QQ、UC 默认局域。局域播放设 x5-playsinline="true"。同层播放设 x5-video-player-type="h5-page"。同层全屏:不设 x5-playsinline,只设 x5-video-player-type="h5-page"。同层全屏的控件前端可以自定义;native 形态是顶层独立 view,会挡住视频区其它元素。
小米 10 实测(lizhao,2020/05/30):微信、QQ 浏览器在横屏全屏点「旋转」回到竖屏时页面样式会乱;UC 正常。
伪全屏:保持局域播放(playsinline,注意兼容),用样式把视频撑满屏幕。适合全屏还要叠弹幕、评论。
用 CSS 藏控制条:外层 div 固定高度并 overflow: hidden,video 高度大于外层,把控制条顶出可视区。画面会被放大,视频要留安全区,避免裁掉主体。
video 事件兼容性
兼容较好的有 ended、timeupdate、play、playing 等。同一事件在不同系统、设备、浏览器上并不一致。按业务需要的兼容面来接,少堆事件,少踩坑。
进入、退出全屏:
// 腾讯浏览器(TBS >= 036900,QQ 浏览器 >= 7.2)
video.addEventListener("x5videoenterfullscreen", () => {
console.log("进入全屏通知");
})
video.addEventListener("x5videoexitfullscreen", () => {
console.log("退出全屏通知");
})
// iOS 微信
video.addEventListener("webkitbeginfullscreen", () => {
console.log("进入全屏通知");
})
video.addEventListener("webkitendfullscreen", () => {
console.log("退出全屏通知");
})
video.addEventListener('pause', () => {
console.log('暂停了')
})
video.addEventListener('ended', () => {
console.log('停止了')
})
事件在不同环境的表现:
| 事件 | PC | iOS | Android |
|---|---|---|---|
loadstart |
开始加载,尚未拿到数据 | 一致 | 一致 |
stalled |
没在播、没取到媒介数据(多为网络中断) | 一致 | 可能在 play() 之前 |
play |
开始播放的意图,画面未必已出 | 一致 | 一致 |
waiting |
play() 之后等数据 |
一致 | 一致 |
durationchange |
拿到真实时长 | 一致 | 可能在 play() 前;可能多次,只有最后一次是真时长,之前可能是 0 或 1 |
loadedmetadata |
play() 之后拿到元数据 |
一致 | play() 前,元数据往往还不是真的 |
loadeddata |
play() 之后拿到媒介数据 |
一致 | play() 前,媒介数据往往还不是真的 |
canplay |
可以播,中途仍可能因加载暂停 | 一致 | 一致 |
playing |
开始出画面 | 一致 | 可能还没真正开播,时长也可能还没有 |
canplaythrough |
开播后可流畅播 | 一致 | 数据可能还没加载,画面可能还没动,仍会卡住 |
timeupdate |
进度明确变化,可读 currentTime |
一致 | 第一次可能有误差;currentTime 变了,说明已经开始播 |
progress |
持续下载,可读 buffer,下完不再触发 | 一致 | 第一次可能有误差;下完仍可能继续触发 |
suspend |
缓冲中;下完不再触发。尚未真实播放时 pause() 也会触发 |
一致 | 一致 |
pause |
响应 pause(),或切出页面自动暂停 |
一致 | 一致 |
seeking |
拖进度或播完找下一段 | 一致 | 一致 |
seeked |
定位完成、下一段开始、或循环从头 | 一致 | 一致 |
error |
出错,原因不易定位,无法再靠 play() 继续 |
一致 | 一致 |
关于 X5 内核
微信内置浏览器内核:
- Android:微信 6.1 以上用 QQ 浏览器 X5。5.4-6.1 之间,装了 QQ 浏览器就用 X5,否则用系统内核。
- iOS:WKWebView 或 UIWebView,iOS 8 起支持 WKWebView。
最简单的检测方法:打开网页下拉,顶部有「QQ浏览器X5内核提供技术支持」就是 X5。
关闭、禁用、安装 X5:
- 微信打开
http://debugtbs.qq.com进调试页。 - 未安装则打不开。已安装可点左上「查看版本号」,内核版本为 0 表示已关闭。
- 点「安装线上内核」,下载或安装中强关微信,可以禁用 X5。
- 安装:打开
https://debugmm.qq.com/?forcex5=true。 - 关闭 X5 不会让微信不能用,有时整体更流畅,因为部分原生界面也要靠 WebView。
播放形态:
- 全屏:默认。点视频后进全屏,事件由 X5 组件托管,层级最高,挡住该区域所有 HTML。适用于安卓微信、手机 QQ 等非安卓 QQ 浏览器的 X5 场景。
- 页面内:设
x5-playsinline,限定在网页内播。 - 同层页面内:设
x5-video-player-type="h5-page",可在视频上方叠 HTML。
其它特性:
- 竖屏播视频时横放手机会进横屏全屏,再竖回来不会自动切回,要用户点全屏上的返回。这是产品策略。
- 不支持多个视频同时播,也不支持和音频同时播。
- 同层全屏不会盖住网页 HTML;native 播放器是顶层独立 view,会遮盖视频区其它元素。
currentTime只有开始播放后设置才有效。- 不支持字幕。
音视频同步播放
需求:同一视频面向不同客户时,把音轨里一段话换成客户姓氏(例如「先生,您好」换成「李先生,你好」)。
思路一:服务端直接改视频音轨。不可行。40MB 视频走分离、改音轨、合成、上传大约 3 分钟,太慢。
思路二:拆成无声音视频 + 音频,服务端只处理音频;前端同步播两者。可行,但要解决下面几个问题。
问题一:安卓微信不能同时播一个视频和一个音频
这是 X5 限制。不能要求用户去关 X5。
调研过的路:
- 拆成多个小视频串行播:体验不好。下一段会停在前一段海报上等待加载,进度也不好比。
- 用 canvas 播视频:不可行。canvas 是靠定时器或重绘去抓
video的每一帧,仍然依赖video。适合给画面做模糊背景、截图、去色。 - 用 Web Audio API 播音频:可行,见下一节。可以和
video同时播。 - 用
embed、object代替:不可行。类似嵌一个带video、audio的 iframe,仍避不开双播限制;音频不好控;会出控件界面(安卓小窗,iOS 全屏);部分属性不可用(如embed的starttime)。 - 小程序:当时未调研,取决于是否放开音视频同时播的限制。
问题二:视频和音频如何同步
同时开播:加载耗时不同,要等两边都能播。用 canplay 标记可播,再用定时器轮询,两边都可播再真正 play()。部分机型必须用户手势才能播,定时器里直接 play() 会无效。做法是:点击时先播一次,定时器里再判断暂停或继续。
拖进度后仍同步:改 currentTime 后两边加载时间又不同。此时不能靠 canplay,因为 iOS 改 currentTime 后不触发 canplay。改用 seeked 判断是否加载完。
canplay、seeked 实测(一台 iOS、一台安卓):
| 事件 | iOS App | iOS 微信 | Android App | Android 微信 |
|---|---|---|---|---|
canplay(开始播放) |
触发 | 触发 | 触发 | 触发 |
canplay(改 currentTime 后) |
不触发 | 不触发 | 触发 | 触发 |
seeked |
触发 | 触发 | 触发 | 触发 |
纠正时差:按理两边加载完再同时播不应有时差,部分机型仍会偏,有时几十秒,原因未定位。用定时器盯两边时间,超过阈值(如 0.5s)就改音频的 currentTime。
问题三:无音轨视频
部分 iOS 播无音轨视频要等很久才开始(安卓、PC Chrome 无此问题)。40MB 视频:无音轨要加载三十多 MB(约 20-30 秒,跟网速有关)才开播;同一视频保留音轨(音量调 0)加载 1-2 MB 就能开播。原因未定位。
Web Audio API
安卓微信(X5)不允许 video 和 audio 同时播。Web Audio API 可以控音频,且能和 video 同时播。
const URL = 'path/music.mp3'
const audioContext = new (window.AudioContext || window.webkitAudioContext)()
const getBuffer = function (url) {
const request = new XMLHttpRequest();
return new Promise((resolve, reject) => {
request.open('GET', url, true);
request.responseType = 'arraybuffer';
request.onload = () => {
audioContext.decodeAudioData(request.response, buffer => buffer ? resolve(buffer) : reject('decoding error'));
};
request.onerror = error => reject(error);
request.send();
});
};
const playAudio = function (buffer) {
const source = audioContext.createBufferSource();
source.buffer = buffer;
source.connect(audioContext.destination);
source.start();
};
const buffer = await getBuffer(URL);
buffer && playAudio(buffer)
现行也可以用 fetch(url).then(r => r.arrayBuffer()) 再 decodeAudioData,逻辑相同。
注意:
AudioContext.currentTime不是音频播放进度,而是「当前时间 − 上下文创建时间」。- 按标准文档改音量、静音,实测有问题。
- 有的浏览器最多同时 6 个
AudioContext。无用的要close()。见 otalk/hark#38。 - 部分机型(如乐视 Android 6.0)App 里
decodeAudioData解不了,微信里正常。 - 部分机型(如 vivo X7 Android 5.1.1)不支持 Howler(对 Web Audio 的封装)。
兼容问题
播放与暂停:切出、锁屏
切出应用再回来、锁屏再解锁,video、audio、Web Audio 的表现按系统(iOS、Android)和应用(微信、保险师等)不一致。
- 切出微信时,
video常自动停,audio仍在播。并非所有机型如此。安卓腾讯 X5 处理较完整:切出后台两者都停,切回后续播。 - 切出时,部分机型 Web Audio 不停;即使在
visibilitychange里调stop也停不了。 - 切回时,部分机型(如华为 P30 Android 9)
video会续播,Web Audio 不会。
切出微信 audio 不停:在 visibilitychange 里 pause。Document.visibilityState 表示标签页是否可见:
document.addEventListener("visibilitychange", e => {
if (document.visibilityState === 'hidden') {
video.pause()
}
if (document.visibilityState === 'visible') {}
if (e.target.hidden) {}
})
切回后三者步调仍不一致时,可在 visibilityState === 'visible' 时刷新页面,或重建音视频模块。
其它属性、事件
canplay、canplaythrough:iOS 加载阶段不触发(preload="auto" 也没用),要播放后才触发;readyState 也是播后才有值。Android、PC Chrome 会在加载阶段触发。改 currentTime 后,Android 会触发 canplay,iOS 不会。
loadedmetadata:安卓微信(华为 Mate 8)取到的 duration、videoWidth、videoHeight 为 0。改在 timeupdate 里取。
muted:部分安卓微信(华为 Mate 8)标签上写了 muted 后,再用 JS 改无效。标签不要写 muted,初始化时用 JS 赋值。
volume:iOS 不能靠 volume 改视频音量,写在标签上或用 JS 改都不行。
poster:浏览器里还行,微信和 iOS 可能空白。办法一:自定义 div 盖封面。办法二:用 canvas 截第一帧。
function cut () {
let canvas = document.createElement("canvas");
canvas.width = video.videoWidth * scale;
canvas.height = video.videoHeight * scale;
canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
let img = document.createElement("img");
img.src = canvas.toDataURL("image/png");
Dom.appendChild(img);
};
video.addEventListener('loadeddata', cut)
安卓其它问题
播完弹出广告:在 ended 里先 play() 再马上 pause()。
let isiOS = !!navigator.userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/)
let video = document.getElementById('video');
video.addEventListener('ended', () => {
if (!isiOS) {
video.play();
setTimeout(() => {
video.pause();
}, 100)
}
})
安卓微信(如小米 10)局域播放仍有「全屏」按钮:X5 劫持了 video。公开做法:成为腾讯合作伙伴;或把 width 撑到如 120%,再用 object-position 把画面算到中间,两边裁掉。
结语
<video> 的标准属性、事件在桌面大体够用。要在微信、QQ 里局域播放、自动播放、音画同步,还得加上 playsinline、X5 属性和各端自己的事件顺序。先定播放形态,再按机型补策略,比堆满事件更稳。