media-autoplay-kit
v0.1.1
Published
Framework-agnostic audio and video autoplay priming for mobile browsers and embedded webviews.
Maintainers
Readme
media-autoplay-kit
English | 简体中文
一个与框架无关的音频和视频自动播放预激活工具,适用于移动端浏览器、内嵌 WebView 和 iframe 应用。
为什么需要这个包
当 audio.play() 或 video.play() 发生在接口请求、定时器或其他异步操作之后,浏览器可能拒绝有声音的媒体播放。这个包提供了一套统一的处理流程:
- 创建一个长期复用的媒体服务。
- 在可信的点击、触摸或键盘事件中直接调用
prime()。 - 等真实音视频地址返回后,继续复用同一个媒体元素。
- 监听
blocked和error状态,必要时显示手动播放按钮。 - 明确地停止、隐藏、清理或销毁服务。
这个包还会:
- 当新播放请求已经开始时,忽略旧的
play()异步结果; - 区分自动播放被拒绝(
NotAllowedError)和资源加载失败; - 通过
removeAttribute('src')安全清理媒体地址; - 在隐藏位置和可见容器之间移动同一个持久化视频元素;
- 延迟创建 DOM 元素,因此仅导入模块时支持 SSR;
- 不依赖 Vue、React 或其他运行时框架。
本包只使用标准 Web 媒体 API,不包含
WeixinJSBridge或其他平台专用的自动播放绕过逻辑。
安装
pnpm add media-autoplay-kit包同时提供 ESM、CommonJS 和 TypeScript 类型声明。
应该选择哪个 API
| 使用场景 | 推荐 API |
| ------------------------------ | ------------------------------------------------ |
| 同一个模块同时管理音频和视频 | createMediaAutoplayKit() |
| 音频和视频由不同模块管理 | AudioAutoplayService 和 VideoAutoplayService |
| 只需要记录用户手势的时间和序号 | GestureTracker |
| 正在迁移已有的单例服务 | 使用单独的服务创建兼容适配器 |
每个独立的播放区域只创建一个服务实例。不要在每次渲染或每次播放时创建新实例。
核心使用流程
prime() 必须在可信用户事件的同步执行阶段开始调用。在调用它之前,不要先等待其他异步任务。
import { createMediaAutoplayKit } from 'media-autoplay-kit'
const media = createMediaAutoplayKit()
const unlockButton = document.querySelector<HTMLButtonElement>('#unlock-media')!
unlockButton.addEventListener('pointerdown', () => {
// 音频和视频的预激活会在事件处理函数让出执行权之前开始。
void media.prime()
})
// 真实播放可以晚一些发生,例如等待接口返回之后。
const audioStarted = await media.audio.play('/voice.mp3')
const videoStarted = await media.video.play('/intro.mp4')
if (!audioStarted && media.audio.state.blocked) {
showAudioPlayButton()
}
if (!videoStarted && media.video.state.blocked) {
showVideoPlayButton()
}media.prime() 会先同时发起音频和视频的预激活,再等待两个结果:
const result = await media.prime()
// { audio: boolean, video: boolean }
console.log(result)也可以只预激活一种媒体:
await media.prime({ audio: true, video: false })
await media.prime({ audio: false, video: true })recordGesture() 只记录序号和时间戳。它不会调用 play(),因此单独调用它不能解除浏览器的自动播放限制。
音频服务
import { AudioAutoplayService } from 'media-autoplay-kit'
const audio = new AudioAutoplayService()
button.addEventListener('pointerdown', () => {
void audio.prime()
})
const started = await audio.play('/answer.wav', {
onStart: () => console.log('音频开始播放'),
onEnded: () => console.log('音频播放结束'),
onTimeUpdate: (currentTime, duration) => {
console.log({ currentTime, duration })
},
onError: (error) => {
console.error('音频播放失败', error)
},
onStop: () => console.log('音频已停止')
})
if (!started && audio.state.blocked) {
showTapToPlayButton()
}
audio.setMuted(true)
audio.stop()音频 API
| 成员 | 作用 |
| -------------------------- | -------------------------------------------------------------- |
| prime() | 播放一个短小但格式有效的静音 WAV,提高后续有声音播放的兼容性。 |
| play(source, callbacks?) | 播放指定资源;当当前请求成功开始播放时返回 true。 |
| pause() | 暂停播放,但不清理资源地址。 |
| stop() | 暂停、重置播放时间、清理资源,并触发上一次播放的 onStop。 |
| clearSource() | 清理资源地址,但保留可继续复用的服务和元素。 |
| setMuted(muted) | 修改媒体元素的静音状态。 |
| subscribe(listener) | 监听只读状态快照,并返回取消监听函数。 |
| element | 延迟创建并返回持久化的 HTMLAudioElement。 |
| currentSource | 返回当前资源地址;没有资源时为 null。 |
| isPlaying | 返回当前状态是否为 playing。 |
| destroy() | 移除事件监听和服务创建的元素;销毁后实例不能再次使用。 |
持久化视频服务
视频服务始终管理同一个 HTMLVideoElement。attach() 把它移动到可见容器中,hide() 则把同一个元素移回隐藏位置。
import { VideoAutoplayService } from 'media-autoplay-kit'
const video = new VideoAutoplayService({
id: 'product-video'
})
submitButton.addEventListener('pointerdown', () => {
void video.prime()
})
const started = await video.play('/widget-video.mp4', {
onStart: () => console.log('视频开始播放'),
onEnded: () => console.log('视频播放结束'),
onError: (error) => console.error('视频播放失败', error)
})
if (started) {
video.attach(document.querySelector<HTMLElement>('#video-host')!, {
controls: true,
orientation: 'landscape'
})
}
// 暂停视频,并把同一个元素移出可见区域。
video.hide(true)视频 API
| 成员 | 作用 |
| ----------------------------- | ------------------------------------------------------ |
| prime() | 使用静音资源预激活持久化视频元素。 |
| play(source?, callbacks?) | 播放新资源,或继续播放当前资源。 |
| pause(resetTime?) | 暂停视频,并可选择把 currentTime 重置为 0。 |
| attach(container, options?) | 把持久化视频元素移动到可见容器中。 |
| hide(pause?) | 应用隐藏样式、移动到隐藏父元素,并可选择暂停。 |
| clearSource() | 清理当前资源,但保留可继续复用的元素。 |
| subscribe(listener) | 监听视频状态,并返回取消监听函数。 |
| element | 延迟创建并返回持久化的 HTMLVideoElement。 |
| destroy() | 移除事件监听和服务创建的元素;销毁后实例不能再次使用。 |
orientation: 'landscape' 会让视频尺寸以容器宽度为主;orientation: 'portrait' 会让视频尺寸以容器高度为主。
状态和错误处理
音频和视频的 play() 都会返回:
true:最新的播放请求成功开始;false:播放被拒绝、发生错误,或者该请求已被更新的请求取代。
可以根据状态决定界面应该显示什么:
const unsubscribe = video.subscribe((state) => {
if (state.blocked) {
showManualPlayControl()
}
if (state.status === 'error') {
reportMediaError(state.error)
}
})
// 不再监听时:
unsubscribe()常见状态如下:
| 状态 | 含义 |
| ----------- | ------------------------------------- |
| idle | 当前没有播放任务。 |
| priming | 正在执行静音预激活。 |
| ready | 预激活成功。 |
| loading | 正在启动真实媒体资源。 |
| playing | 已成功开始播放。 |
| paused | 播放已暂停。 |
| blocked | 浏览器以 NotAllowedError 拒绝播放。 |
| error | 发生了非权限类播放错误或资源错误。 |
| destroyed | 服务已经被永久销毁。 |
primed 和 playing 不是同一个概念:
primed: true表示静音预激活成功;status === 'playing'表示真实媒体当前正在播放。
预激活只能提高兼容性,无法覆盖浏览器设置、宿主应用策略或 iframe 的 Permissions Policy。
Vue 3 集成
本包不依赖 Vue。建议使用 markRaw() 防止 Vue 深度代理外部服务实例,并使用 shallowRef() 暴露状态。
import { markRaw, onScopeDispose, shallowRef } from 'vue'
import { VideoAutoplayService } from 'media-autoplay-kit'
export function useOwnedVideoAutoplay() {
const video = markRaw(new VideoAutoplayService())
const state = shallowRef(video.state)
const unsubscribe = video.subscribe((nextState) => {
state.value = nextState
})
onScopeDispose(() => {
unsubscribe()
video.destroy()
})
return { video, state }
}如果多个组件共享播放能力,请在全局 store、Provider 或模块单例中创建服务。只有当这个拥有者被永久销毁时,才调用 destroy()。
迁移已有服务
可以通过兼容适配器保留旧调用方式,同时让本包负责实际播放:
import { AudioAutoplayService } from 'media-autoplay-kit'
const audio = new AudioAutoplayService()
export const legacyAudioService = {
handlePreload: () => audio.prime(),
play: (source: string, onStart?: () => void, onEnded?: () => void) =>
audio.play(source, { onStart, onEnded }),
stop: () => audio.stop(),
clearSrc: () => audio.clearSource(),
setMuted: (muted: boolean) => audio.setMuted(muted),
getPlayedStatus: () => audio.isPlaying,
getPrimedStatus: () => audio.state.primed
}迁移对应关系:
| 旧方法 | 新方法 |
| ------------------------------- | -------------------------------------------- |
| audioService.handlePreload() | audio.prime() |
| audioService.play(...) | audio.play(source, callbacks) |
| audioService.stop() | audio.stop() |
| audioService.clearSrc() | audio.clearSource() |
| recordMediaPlaybackGesture() | gestures.record() 或 kit.recordGesture() |
| primeMediaPlaybackVideo() | video.prime() |
| playMediaPlaybackVideo(url) | video.play(url) |
| attachMediaPlaybackVideo(...) | video.attach(...) |
| hideMediaPlaybackVideo(...) | video.hide(...) |
构造参数
音频和视频共同支持:
| 参数 | 含义 |
| -------------- | ---------------------------------------------------------------------- |
| document | 显式指定浏览器 Document,适合 iframe 或测试环境。 |
| element | 使用已有的音频或视频元素;服务不会删除外部传入的元素。 |
| appendTo | 隐藏元素的父节点,默认是 document.body;传入 null 可禁止自动插入。 |
| preload | 可选 auto、metadata 或 none,默认是 auto。 |
| silentSource | 自定义静音预激活资源,默认使用包内导出的有效 WAV data URI。 |
视频服务还支持 id,默认值为 media-autoplay-kit-video。
iframe 和 WebView 注意事项
在 iframe 中使用时,父页面可能需要明确授予自动播放权限:
<iframe src="https://example.com/media-page" allow="autoplay"></iframe>还需要检查嵌入页面的 Permissions-Policy 响应头。本包可以报告播放被拒绝,但不能修改父页面的权限策略。
请在真实目标环境中测试。桌面浏览器成功,并不代表 iOS Safari、Android Chrome 或内嵌 WebView 的行为完全一致。
重要限制
- 必须从可信用户交互中直接开始调用
prime()。 - 应长期保留并复用同一个服务实例。
- 当
state.blocked为true时,始终提供可见的手动播放入口。 - 有声音的自动播放通常比静音视频限制更严格。
- 本包不会注册全局手势监听器。
destroy()是永久操作;销毁后需要创建新服务。- 导入模块支持 SSR,但调用
element、prime()或play()时必须存在浏览器Document。
发布前测试
构建并验证包:
pnpm check
pnpm pack:check然后把实际 tarball 安装到业务项目中测试:
pnpm pack
pnpm add /absolute/path/media-autoplay-kit-0.1.1.tgz使用模拟 HTMLMediaElement.play() 的单元测试可以验证状态逻辑,但只有真实浏览器和设备才能验证自动播放策略。
开源协议
本项目基于 MIT License 开源。
开发命令
pnpm install
pnpm check
pnpm test
pnpm build
pnpm pack:check