cl-video 自动获取封面的视频组件
插件地址直达 一个基于 uni-app 的跨端视频组件,支持自动提取视频首帧作为封面。
功能特性
- ✅ 自动提取视频首帧作为封面
- ✅ 支持跨端使用(H5、App)
- ✅ 支持自定义封面
- ✅ 支持设置封面质量
- ✅ 支持封面类型为 path(h5 为 blobUrl)或 base64
基础用法
<template>
<cl-video src="https://example.com/video.mp4" @getPoster="handleGetPoster" />
</template>
<script>
export default {
methods: {
handleGetPoster(poster) {
console.log('获取到的封面:', poster)
},
},
}
</script>
Props 属性
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| src | String | '' | 视频地址(必填) |
| poster | String | '' | 自定义封面地址,如果设置则不会自动提取 |
| posterType | String | 'base64' | 封面格式,可选值:base64、pathbase64: 返回 base64 字符串path: APP 保存到本地文件并返回路径,h5 返回为 blobUrl |
| posterQuality | Number | 0.8 | 封面质量,取值范围:0~1 值越大质量越高,文件也越大 |
注意:组件支持
v-bind="$attrs"透传,可以传入 uni-app video 组件的所有原生属性(如controls、autoplay、muted等)
Events 事件
@getPoster
视频封面提取成功时触发
参数:
poster(String): 封面数据- H5 端:返回 base64 字符串
- App 端:
posterType='base64'时返回 base64 字符串posterType='path'时返回本地文件绝对路径
组件源码
<template>
<video :src="src" :poster="aotoPoster" v-bind="$attrs"></video>
<!-- 空白属性用于触发renderjs,注意这里的属性的顺序会影响renderjs里面方法的调用顺序,所以src一定要在最后 -->
<view
:poster="poster"
:change:poster="video.setRenderjsPoster"
:posterType="posterType"
:change:posterType="video.setRenderjsPosterType"
:posterQuality="posterQuality"
:change:posterQuality="video.setRenderjsPosterQuality"
:src="src"
:change:src="video.setPoster"
style="display: none"
></view>
</template>
<script>
export default {
props: {
// 视频地址
src: {
type: String,
default: '',
},
// 视频封面
poster: {
type: String,
default: '',
},
// 封面格式,base64 或 path
// b64就是将canvas转为base64字符串,注意对一些高清大图可能不是太合适
// path就是将canvas转为图片路径存到本地,使用地址的方式使用,注意可能会有权限问题
posterType: {
type: String,
default: 'base64',
},
// 封面质量 0~1
posterQuality: {
type: Number,
default: 0.8,
},
},
data() {
return {
// 从视频里面自动获取到的封面
aotoPoster: '',
}
},
methods: {
getPoster(poster) {
this.$emit('getPoster', poster)
this.aotoPoster = poster
},
// 设置封面失败
setPosterError(error) {
this.$emit('setPosterError', error)
},
},
}
</script>
<script lang="renderjs" module="video">
export default {
data() {
// 为了统一数据结构,这里使用 renderjsData 来存储逻辑层的数据
return {
renderjsData: {
posterType: 'base64',
posterQuality: 0.8,
poster: ''
}
}
},
methods: {
setRenderjsPosterType(newV, oldV) {
this.renderjsData.posterType = newV;
},
setRenderjsPosterQuality(newV, oldV) {
this.renderjsData.posterQuality = newV;
},
setRenderjsPoster(newV, oldV, ownerInstance) {
this.renderjsData.poster = newV;
},
setPoster(newV, oldV, ownerInstance) {
const {posterQuality, poster, posterType} = this.renderjsData
// // 如果有封面或者没有视频地址则直接返回
if (poster || !newV) {
return
}
// 1. 创建隐藏的视频对象用于截帧
let video = document.createElement("video");
video.setAttribute('crossOrigin', 'anonymous');
video.setAttribute('src', newV);
video.setAttribute('preload', 'auto');
video.muted = true; // 必须静音,避免占用音频焦点和自动播放限制
video.style.display = 'none';
// 2. 监听第一帧加载完成
video.addEventListener('loadeddata', () => {
let canvas = document.createElement("canvas");
let width = video.videoWidth;
let height = video.videoHeight;
canvas.width = width;
canvas.height = height;
let ctx = canvas.getContext("2d");
ctx.drawImage(video, 0, 0, width, height);
let base64 = canvas.toDataURL('image/jpeg', posterQuality);
// 3. 根据类型获取图片数据
if (posterType === 'path') {
// #ifdef APP-PLUS
const bitmap = new plus.nativeObj.Bitmap('video_poster_' + Date.now());
bitmap.loadBase64Data(base64, () => {
const url = '_doc/poster.jpg';
bitmap.save(url, { overwrite: true, quality: 100 }, (e) => {
// 关键:转换为原生组件可识别的绝对路径
const absolutePath = plus.io.convertLocalFileSystemURL(url);
ownerInstance.callMethod('getPoster', absolutePath);
// 4. 清理资源:Bitmap 和 Video 对象都要销毁
bitmap.clear();
this.disposeVideo(video);
});
}, (error) => {
ownerInstance.callMethod('setPosterError', error);
this.disposeVideo(video);
});
// #endif
// #ifdef H5
const blobUrl = this.base64ToTempUrl(base64, ownerInstance);
if (blobUrl) {
ownerInstance.callMethod('getPoster', blobUrl);
}
this.disposeVideo(video);
// #endif
} else {
ownerInstance.callMethod('getPoster', base64);
this.disposeVideo(video);
}
});
video.addEventListener('error', () => {
this.disposeVideo(video);
});
// 尝试播放以触发加载(部分安卓机型 preload 不会自动开始缓冲)
video.play().catch(() => {});
},
// 彻底销毁隐藏视频对象的方法
disposeVideo(video) {
if (video) {
video.pause();
video.src = '';
video.load();
video.remove();
}
},
base64ToTempUrl(base64Str, ownerInstance) {
try {
// 1. 校验base64格式(基础校验)
if (!base64Str || !base64Str.startsWith('data:image/')) {
throw new Error('无效的base64图片格式')
}
// 2. 拆分base64的类型和内容部分
const [metaData, base64Data] = base64Str.split(',')
const mime = metaData.match(/:(.*?);/)[1] // 提取图片类型(如image/jpeg)
// 3. 将base64解码为二进制数据
const byteCharacters = atob(base64Data)
const byteNumbers = new Array(byteCharacters.length)
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i)
}
const byteArray = new Uint8Array(byteNumbers)
// 4. 创建Blob对象并生成临时URL
const blob = new Blob([byteArray], { type: mime })
const tempUrl = URL.createObjectURL(blob)
return tempUrl
} catch (error) {
ownerInstance.callMethod('setPosterError', error);
return ''
}
},
}
}
</script>