前言
最近我把开源项目 vue-aliplayer-v2 做了一次比较彻底的重构:从原来的 Vue 2 组件升级到 Vue 3 + TypeScript + Vite,并围绕阿里云新版 Web 播放器 SDK 补齐了 License、低延迟直播、VID + PlayAuth、STS、自动格式推断、多实例、扩展组件脚本、失败重试、双语 README 和 demo 工作台。
这次重构没有选择继续兼容 Vue 2。原因很简单:如果一边照顾旧版本 API,一边接入新版 SDK 和 Vue 3 组件模型,项目会越来越难维护。旧项目如果仍然依赖 Vue 2,可以继续固定安装 vue-aliplayer-v2@1.x;从 v2 开始,目标就是把组件做好、类型补全、使用体验理顺。
项目地址:
text
https://github.com/langyuxiansheng/vue-aliplayer-v2
在线 demo:
text
https://langyuxiansheng.github.io/vue-aliplayer-v2/
为什么要重构
这个项目的核心职责是把阿里云 Aliplayer Web SDK 封装成一个 Vue 组件。早期版本能解决基本播放问题,但随着 Vue 生态和阿里云播放器 SDK 的变化,旧实现会遇到几个典型问题:
- Vue 2 插件写法已经不适合 Vue 3 项目。
- 新版阿里云 Web 播放器需要 License 配置。
- SDK 资源路径已经从旧版播放器资源迁移到
imp-web-player。 - 直播、FLV、M3U8、RTMP、VID、STS 等场景需要更明确的初始化逻辑。
- 多播放器实例共存时,SDK 加载和 DOM 容器管理需要更稳。
- 原项目文档和 demo 比较弱,很多参数只能靠翻源码。
- TypeScript 类型不完整,业务侧使用 ref 调方法时不够清晰。
所以这次重构的目标不是"让旧代码能跑",而是重新梳理组件边界:组件负责 Vue 生命周期、SDK 资源加载、基础兼容和常用能力暴露;真正的播放能力仍然交给阿里云官方 SDK。
技术栈选择
重构后的项目使用:
text
Vue 3
TypeScript
Vite
vue-tsc
Aliplayer Web SDK 2.37.0
构建产物分成两类:
text
dist/ 用于 GitHub Pages demo
lib/ 用于 npm 包发布
Vite 配置里通过 mode 区分 demo 和 lib:
ts
export default defineConfig(({ mode }) => {
const isDemoBuild = mode === 'demo';
return {
base: isDemoBuild ? '/vue-aliplayer-v2/' : '/',
plugins: [vue()],
publicDir: isDemoBuild ? 'public' : false,
build: isDemoBuild
? {
outDir: 'dist',
emptyOutDir: true
}
: {
outDir: 'lib',
emptyOutDir: false,
lib: {
entry: resolve(__dirname, 'packages/index.ts'),
name: 'VueAliplayerV2',
fileName: 'vue-aliplayer-v2'
},
rollupOptions: {
external: ['vue'],
output: {
exports: 'named',
globals: {
vue: 'Vue'
}
}
}
}
};
});
这样做的好处是:demo 和 npm 包互不污染。demo 可以带页面样式和演示逻辑,库包只输出组件代码、样式和声明文件。
组件 API 设计
v2 暴露的组件叫 VueAliplayerV2,支持全局注册和局部引入。
全局注册:
ts
import { createApp } from 'vue';
import App from './App.vue';
import VueAliplayerV2 from 'vue-aliplayer-v2';
const app = createApp(App);
app.use(VueAliplayerV2, {
sdkVersion: '2.37.0'
});
app.mount('#app');
局部使用:
vue
<template>
<VueAliplayerV2
ref="playerRef"
:source="source"
:options="options"
:license="license"
low-latency
@ready="handleReady"
@error="handleError"
@sdk-error="handleSdkError"
/>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
import VueAliplayerV2, {
type AliplayerLicense,
type AliplayerOptions,
type VueAliplayerV2Expose
} from 'vue-aliplayer-v2';
const playerRef = ref<VueAliplayerV2Expose | null>(null);
const source = ref('//player.alicdn.com/video/aliyunmedia.mp4');
const license = ref<AliplayerLicense | null>({
domain: 'example.com',
key: 'your-license-key'
});
const options = reactive<AliplayerOptions>({
autoplay: true,
useH5Prism: true,
playsinline: true,
width: '100%',
height: '420px'
});
function handleReady() {
playerRef.value?.play();
}
function handleError(error: unknown) {
console.log('player error', error);
}
function handleSdkError(error: Error) {
console.log('sdk load error', error.message);
}
</script>
这里我把播放器配置分成了两层:
options:原样透传给阿里云 Aliplayer SDK。- 组件 props:封装 wrapper 层更常用的能力,比如
source、license、autoFormat、lowLatency、componentScripts。
这样既不会限制用户使用官方 SDK 新参数,又能把常见问题在组件层解决掉。
为了让 API 更容易扫读,我把 v2 主要 props 整理成下面这张表。实际使用时可以把它当成接入 checklist。
| Prop | 类型 | 默认值 | 使用场景 | 说明 |
|---|---|---|---|---|
source |
`string | null` | null |
URL 播放、动态切源 |
options |
`AliplayerOptions | null` | null |
透传官方 SDK 参数 |
license |
`AliplayerLicense | null` | null |
新版 Web 播放器 License |
autoFormat |
boolean |
true |
自动识别播放格式 | 根据 URL 后缀推断 mp4、m3u8、flv、mp3、rtmp,用户显式传 options.format 时不覆盖。 |
lowLatency |
boolean |
false |
FLV 直播低延迟 | 在 isLive + flv 场景下默认关闭 stash buffer,并设置较小初始缓存。 |
normalizeSourceUrl |
boolean |
true |
中文、空格 URL | 默认对 URL 执行 encodeURI,处理常见未编码播放地址。 |
forbidFastForward |
boolean |
false |
试看、防快进 | 监听进度变化,发现异常快进时回退到上一次播放位置。 |
sdkVersion |
string |
2.37.0 |
切换阿里云 SDK 版本 | 未传 cssLink、scriptSrc 时用于生成官方资源地址。 |
cssLink |
string |
官方 CSS | 私有部署、固定资源 | 覆盖默认播放器样式地址。 |
scriptSrc |
string |
官方 JS | 私有部署、固定资源 | 覆盖默认播放器脚本地址。 |
componentScripts |
string[] |
[] |
跑马灯、水印、播放列表 | 在播放器初始化前加载额外组件脚本。 |
disableTracking |
boolean |
false |
可选拦截统计请求 | 开启后会安装 wrapper 层的 track 请求拦截器。 |
trackingUrlPatterns |
`Array<string | RegExp>` | 默认 track 规则 | 自定义统计拦截 |
事件也尽量保持和 Aliplayer 官方 SDK 一致。组件只是做一层透传,额外增加了 sdk-error,用于区分"SDK 资源加载失败"和"播放器运行时失败"。
| 事件名 | 参数 | 触发时机 | 常见用途 |
|---|---|---|---|
ready |
unknown |
播放器实例创建完成 | 自动播放、读取播放器状态、初始化业务 UI。 |
play |
unknown |
开始播放 | 统计播放开始、切换按钮状态。 |
pause |
unknown |
暂停播放 | 统计暂停、保存播放进度。 |
canplay |
unknown |
媒体可以播放 | 隐藏 loading、展示控制栏。 |
playing |
unknown |
媒体正在播放 | 判断播放恢复、关闭缓冲提示。 |
waiting |
unknown |
播放等待或缓冲 | 展示缓冲状态、排查网络问题。 |
timeupdate |
unknown |
播放进度变化 | 试看、学习进度、业务进度条同步。 |
ended |
unknown |
播放结束 | 自动播放下一集、记录完成状态。 |
error |
unknown |
播放器运行时错误 | 触发 retry()、展示业务错误提示。 |
sdk-error |
Error |
SDK CSS、JS 或扩展脚本加载失败 | 检查 CDN、版本号、网络拦截、私有资源地址。 |
requestFullScreen |
unknown |
进入全屏 | 同步页面布局或埋点。 |
cancelFullScreen |
unknown |
退出全屏 | 恢复页面布局。 |
startSeek |
unknown |
开始拖拽或跳转 | 记录用户 seek 行为。 |
completeSeek |
unknown |
seek 完成 | 刷新业务播放进度。 |
liveStreamStop |
unknown |
直播流停止 | 提示直播结束或尝试重连。 |
onM3u8Retry |
unknown |
M3U8 重试 | 排查 HLS 网络和分片问题。 |
snapshoted |
unknown |
截图完成 | 获取截图结果或提示用户保存。 |
SDK 加载器:避免重复加载和版本写死
播放器组件经常会遇到一个问题:页面里有多个播放器时,如果每个组件都插入一份 SDK 脚本,很容易造成重复请求、全局对象覆盖或者初始化顺序问题。
v2 做了一个独立的 SDK loader:
ts
export const DEFAULT_SDK_VERSION = '2.37.0';
export const DEFAULT_CSS_LINK = `https://g.alicdn.com/apsara-media-box/imp-web-player/${DEFAULT_SDK_VERSION}/skins/default/aliplayer-min.css`;
export const DEFAULT_SCRIPT_SRC = `https://g.alicdn.com/apsara-media-box/imp-web-player/${DEFAULT_SDK_VERSION}/aliplayer-min.js`;
加载时按 URL 缓存 Promise:
ts
const cssPromises = new Map<string, Promise<void>>();
const scriptPromises = new Map<string, Promise<void>>();
这样多个播放器同时挂载时,最终也只会加载同一份 CSS 和 JS。
同时,sdkVersion、cssLink、scriptSrc 都可以通过全局插件配置或单组件 props 覆盖。如果后续阿里云 SDK 发新版本,用户不需要等组件库发版,也可以先自己指定资源地址。
播放源处理:自动识别格式,但不抢业务控制权
Aliplayer 在不同格式之间切换时,行为并不完全一致。比如 MP4 切 MP4 可以优先调用 loadByUrl,但 MP4 切 FLV、M3U8 切 FLV 这种跨格式场景,重建播放器更稳。
v2 里增加了一个简单明确的格式推断:
ts
export type SourceFormat = 'mp4' | 'm3u8' | 'flv' | 'rtmp' | 'mp3' | null;
export function inferSourceFormat(source?: string | null): SourceFormat {
if (!source) return null;
if (/^rtmps?:\/\//i.test(source)) return 'rtmp';
const cleanSource = source.split('?')[0].split('#')[0].toLowerCase();
const matched = KNOWN_FORMATS.find((format) => format && cleanSource.endsWith(`.${format}`));
return matched || null;
}
组件只在用户没有显式设置 options.format 时才自动补充格式。这一点很重要:自动化应该减少使用成本,但不能覆盖业务明确传入的配置。
另外还加了 URL 标准化:
ts
export function normalizeSource(source?: string | null): string | null {
if (!source) return source || null;
if (/^(data|blob):/i.test(source)) return source;
try {
return encodeURI(source);
} catch {
return source;
}
}
很多业务播放地址里会出现中文文件名或空格,直接给 SDK 可能播放失败。默认做 encodeURI 能解决一批实际问题;如果业务已经自己编码,也可以关闭 normalizeSourceUrl。
License:适配新版阿里云 Web 播放器
新版阿里云 Web 播放器通常需要配置 License。v2 支持两种方式:
vue
<VueAliplayerV2
:source="source"
:license="{ domain: 'example.com', key: 'your-license-key' }"
/>
或者直接放到 options:
vue
<VueAliplayerV2
:source="source"
:options="{
license: {
domain: 'example.com',
key: 'your-license-key'
}
}"
/>
组件层的 license prop 优先级更高。这样做是为了方便全局或页面级统一管理 License,同时不破坏 Aliplayer 原始 options。
VID + PlayAuth 和 STS
除了普通 URL,很多阿里云点播场景会用 VID + PlayAuth 或 STS。
VID + PlayAuth:
vue
<VueAliplayerV2
:options="{
vid: '<your-video-id>',
playauth: '<your-playauth>',
authTimeout: 7200
}"
/>
STS:
vue
<VueAliplayerV2
:options="{
vid: '<your-video-id>',
region: 'cn-shanghai',
accessKeyId: '<temporary-access-key-id>',
accessKeySecret: '<temporary-access-key-secret>',
securityToken: '<temporary-security-token>'
}"
/>
这类初始化模式不需要传 source。如果播放器创建后要切换,可以通过 ref 调用:
ts
playerRef.value?.replayByVidAndPlayAuth(vid, playauth);
FLV 直播低延迟
直播是这次重构重点补齐的场景之一。FLV 直播如果追求更低延迟,通常需要调整 stash buffer。
v2 里提供了 lowLatency:
vue
<VueAliplayerV2
source="//example.com/live.flv"
low-latency
:options="{
isLive: true,
autoplay: true
}"
/>
当满足 isLive + flv + lowLatency 时,组件会默认补充:
ts
{
enableStashBufferForFlv: false,
stashInitialSizeForFlv: 128
}
但这里仍然保留业务覆盖权:如果你在 options 里显式传了 enableStashBufferForFlv 或 stashInitialSizeForFlv,组件不会再强行覆盖。
多实例:每个播放器独立,但 SDK 资源共享
多播放器场景容易出两个问题:
- 多个组件争抢同一个 DOM id。
- 多个组件重复插入 SDK 标签。
v2 为每个组件实例生成独立容器 id:
text
vue-aliplayer-v2-${seed}
SDK 资源则通过 loader 层共享 Promise。这样可以做到:
- 每个播放器实例互不干扰。
- 同一页面只加载一次相同 SDK。
- 组件卸载时销毁自己的播放器实例。
多实例使用示例:
vue
<template>
<VueAliplayerV2
v-for="item in sources"
:key="item"
:source="item"
:options="options"
/>
</template>
Ref 方法:给业务侧足够的控制能力
播放器组件不能只靠 props,因为很多操作是命令式的:播放、暂停、seek、全屏、设置音量、切换源、设置试看时间等。
v2 暴露了 VueAliplayerV2Expose 类型:
ts
const playerRef = ref<VueAliplayerV2Expose | null>(null);
playerRef.value?.play();
playerRef.value?.pause();
playerRef.value?.seek(30);
playerRef.value?.setVolume(0.8);
playerRef.value?.requestFullScreen();
playerRef.value?.retry();
如果只给一个方法名列表,读者需要来回翻类型定义。这里更适合放成表格,把参数、返回值和典型用途一起写清楚:
| 方法 | 参数 | 返回值 | 说明 | 典型场景 |
|---|---|---|---|---|
getPlayer() |
无 | `AliplayerInstance | null` | 获取底层 Aliplayer 实例。 |
init() |
无 | Promise<void> |
加载 SDK、扩展脚本并初始化播放器。 | 手动重新初始化,或调试 SDK 加载流程。 |
initPlayer() |
无 | void |
用当前 props 和 options 直接创建播放器实例。 | SDK 已加载完成后,内部初始化播放器。 |
reload(nextSource?) |
nextSource?: string |
Promise<void> |
重载播放器,可传新的 URL。 | 切换清晰度、切换线路、播放器异常后重建。 |
retry(nextSource?) |
nextSource?: string |
Promise<void> |
语义化的失败重试方法,内部逻辑接近 reload。 |
在 error 事件里给用户一个"重试"按钮。 |
play() |
无 | void |
开始播放。 | 自定义播放按钮、ready 后自动播放。 |
pause() |
无 | void |
暂停播放。 | 自定义暂停按钮、页面离开时暂停。 |
replay() |
无 | void |
从头重播当前视频。 | 播放结束后重新观看。 |
seek(time) |
time: number |
void |
跳转到指定秒数。 | 恢复历史进度、章节跳转。 |
getCurrentTime() |
无 | `number | undefined` | 获取当前播放时间,单位秒。 |
getDuration() |
无 | `number | undefined` | 获取视频总时长,单位秒。 |
getVolume() |
无 | `number | undefined` | 获取当前音量。 |
setVolume(volume) |
volume: number |
void |
设置音量,通常范围为 0 到 1。 |
静音、恢复音量、自定义音量控制。 |
loadByUrl(url, time?) |
url: string, time?: number |
void |
通过 URL 切换播放源。 | 同格式播放源切换、指定起播时间。 |
replayByVidAndPlayAuth(vid, playauth) |
vid: string, playauth: string |
void |
使用 VID + PlayAuth 重新播放。 | 点播鉴权信息刷新后继续播放。 |
replayByVidAndAuthInfo(...) |
vid, accId, accSecret, stsToken, authInfo, domainRegion |
void |
使用 MPS 鉴权信息重新播放。 | 兼容阿里云旧鉴权或特殊业务接入。 |
setPlayerSize(width, height) |
width: string, height: string |
void |
设置播放器尺寸。 | 弹窗、布局切换、横竖屏适配。 |
setSpeed(speed) |
speed: number |
void |
设置播放倍速。 | 课程、长视频、用户自定义倍速。 |
setSanpshotProperties(width, height, rate) |
width: number, height: number, rate: number |
void |
设置截图参数,方法名沿用 SDK 原始拼写。 | 业务截图、封面生成。 |
requestFullScreen() |
无 | void |
进入全屏。 | 自定义全屏按钮。 |
cancelFullScreen() |
无 | void |
退出全屏。 | 自定义退出全屏按钮。 |
getIsFullScreen() |
无 | `boolean | undefined` | 获取当前是否全屏。 |
getStatus() |
无 | `string | undefined` | 获取播放器内部状态。 |
setLiveTimeRange(beginTime, endTime) |
beginTime: string, endTime: string |
void |
设置直播时移可播放范围。 | 直播回看、时移播放。 |
setRotate(rotate) |
rotate: number |
void |
设置视频旋转角度。 | 处理拍摄方向不正确的视频。 |
getRotate() |
无 | `number | undefined` | 获取当前旋转角度。 |
setImage(image) |
image: string |
void |
设置视频镜像。 | 摄像头、镜像预览类场景。 |
dispose() |
无 | void |
销毁播放器实例。 | 手动释放资源、弹窗关闭。 |
setCover(cover) |
cover: string |
void |
设置封面图。 | 动态替换视频封面。 |
setProgressMarkers(markers) |
markers: unknown[] |
void |
设置进度条打点。 | 章节点、广告点、知识点标记。 |
setPreviewTime(time) |
time: number |
void |
设置试看时间。 | 付费视频试看。 |
getPreviewTime() |
无 | `number | undefined` | 获取试看时间。 |
isPreview() |
无 | `boolean | undefined` | 判断是否处于试看状态。 |
off(eventName, handler) |
eventName: string, handler: Function |
void |
取消底层播放器事件监听。 | 手动绑定 SDK 事件后清理监听。 |
这里尽量保持了阿里云 SDK 的原始方法名,包括 setSanpshotProperties 这种 SDK 里的拼写。这样用户查官方文档时不会因为 wrapper 改名而产生额外心智负担。
扩展组件脚本:跑马灯、水印、播放列表
Aliplayer 的自定义组件能力通常需要额外脚本,例如跑马灯、水印、播放列表等。
v2 提供 componentScripts:
vue
<VueAliplayerV2
:source="source"
:component-scripts="['/aliplayer-components/marquee.js']"
:options="{
components: [
{
name: 'MarqueeComponent',
type: window.MarqueeComponent,
args: {
text: 'vue-aliplayer-v2'
}
}
]
}"
/>
组件会保证扩展脚本在播放器初始化之前加载完成。这样业务侧不需要在页面里手动管理脚本加载顺序。
可选的 track 上报拦截
部分用户反馈过播放器统计请求的问题,所以 v2 提供了一个可选兜底能力:
vue
<VueAliplayerV2
:source="source"
disable-tracking
:tracking-url-patterns="['newplayer/track', /\/logstores\//]"
/>
实现上会 patch fetch 和 XMLHttpRequest,拦截命中规则的请求。
这个能力我没有默认开启,因为它会影响全局网络 API。生产环境里,优先建议使用阿里云官方配置、控制台设置或业务自己的合规上报策略。组件层的拦截只适合作为兜底方案。
Demo 也要认真写
这次我没有只写一个"能播放视频"的 demo,而是重新做了一个参数工作台。它包含:
- SDK 版本切换
- URL / VID / STS 三种播放源模式
- License 输入
- 自动格式推断
- FLV 低延迟配置
- 直播时移配置
- 皮肤布局配置
- 自定义 options JSON
- 事件流
- 配置预览和复制
- 多实例验证
这样 demo 不只是展示页,也能作为调试工具。遇到用户反馈 issue 时,可以直接在 demo 中复现参数组合。
文档和类型
组件库想要好用,类型和文档必须跟上。
这次做了三件事:
- 所有公开类型补了详细 JSDoc。
- README 写成中英文双语版本,并互相链接。
- npm 包发布文件里包含
README.md和README.en-US.md。
类型导出如下:
ts
import type {
AliplayerEventName,
AliplayerInstance,
AliplayerLicense,
AliplayerOptions,
AliplayerV2Props,
VueAliplayerV2Expose,
VueAliplayerV2Options
} from 'vue-aliplayer-v2';
业务侧不需要进入内部目录找类型,直接从包入口导入即可。
构建和发布
本地开发:
bash
npm install
npm run dev
类型检查:
bash
npm run type-check
构建 demo:
bash
npm run build
构建组件库:
bash
npm run lib
发布前可以先 dry-run 看包内容:
bash
npm pack --dry-run
如果本机 npm cache 权限异常,可以临时指定 cache:
bash
npm --cache /private/tmp/npm-cache-vue-aliplayer-v2 pack --dry-run
这次重构修掉了什么
从功能角度看,这次 v2 主要解决了这些问题:
- Vue 3 项目可以直接使用。
- 新版 Aliplayer SDK 资源路径可配置。
- License 有明确入口。
- URL、VID、STS 三种初始化方式更清晰。
- MP4、M3U8、FLV、MP3、RTMP 格式能自动推断。
- 同格式切源和跨格式切源有不同处理策略。
- 多播放器不会重复加载 SDK。
- 直播 FLV 有低延迟预设。
- 播放失败可以通过
retry()/reload()处理。 - 扩展组件脚本有统一加载入口。
- TypeScript 用户可以获得完整的 ref 方法提示。
- README 和 demo 能覆盖大部分常见使用方式。
一点重构经验
这次重构最大的感受是:组件库升级不要只做语法迁移。
如果只是把 Vue 2 代码改成 Vue 3 写法,短期看起来成本低,但实际价值有限。真正应该做的是重新确认边界:
- 哪些能力应该由 wrapper 负责?
- 哪些能力应该透传给官方 SDK?
- 哪些默认行为是安全的?
- 哪些配置必须保留业务覆盖权?
- demo 能不能帮助用户复现问题?
- 类型和 README 能不能让用户少翻源码?
对播放器组件来说,最重要的是稳定。组件层不应该试图重写播放器能力,而是应该把 SDK 接入、生命周期、资源加载、常见兼容和工程体验处理好。
结语
vue-aliplayer-v2 的 v2 版本现在已经从 Vue 2 时代的封装,升级成了一个面向 Vue 3 的播放器组件库。它不是一个重写播放器内核的项目,而是一个更现代、更清晰、更容易维护的 Aliplayer Vue wrapper。
如果你的项目正在使用 Vue 3,并且需要接入阿里云点播、直播或 Aliplayer Web SDK,可以试试这个版本。
项目地址:
text
https://github.com/langyuxiansheng/vue-aliplayer-v2
在线 demo:
text
https://langyuxiansheng.github.io/vue-aliplayer-v2/