uniapp自动获取视频封面 (支持H5和APP)

103 阅读3分钟

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 属性

参数类型默认值说明
srcString''视频地址(必填)
posterString''自定义封面地址,如果设置则不会自动提取
posterTypeString'base64'封面格式,可选值:base64path
base64: 返回 base64 字符串
path: APP 保存到本地文件并返回路径,h5 返回为 blobUrl
posterQualityNumber0.8封面质量,取值范围:0~1
值越大质量越高,文件也越大

注意:组件支持 v-bind="$attrs" 透传,可以传入 uni-app video 组件的所有原生属性(如 controlsautoplaymuted 等)

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>