乐趣区

关于前端:uniapp项目中如何利用canvas获取视频的封面

记录一下在 uni-app 我的项目中遇到的一些问题。

一、抉择视频并上传,播放

展现:

全屏播放:

1、<video> 在非 H5 端图层太高的问题

安卓 APP 中,因为 <video> 是原生组件,层级高于一般前端组件,笼罩其须要应用 <cover-view>、<cover-image>,但要在 <video> 开始完结标签之间。
自定义播放按钮,删除按钮都能够应用这些标签实现。
但展现抉择的视频时,弹出抉择框(如 picker),就无奈用这些标签实现层级高于 <video>。
实现形式:展现时用 view,播放时用 <video> 全屏播放。

html 局部

<!-- 展现视频 -->
<!-- #ifdef APP-PLUS || H5 -->
<view :id="`${item.id}`" class="video">
  <image class="video-poster" :src="item.posterUrl"/>
  <image v-if="clearable" class="video-clear-btn" src="/static/clear.png" @click="deleteFile(item, index)"/>
  <image class="video-play-btn" src="/static/play.png" @click="toPlayVideo(item.url)"/>
</view>
<!-- #endif -->

<!-- 全屏播放视频 -->
<video
  class="full-play-video"
  id="fullScreenVideo"
  v-if="videoUrl"
  :src="videoUrl"
  object-fit="contain"
  @fullscreenchange="fullscreenchange"
  @error="onVideoError"
/>

js 局部

   mounted() {   
     // #ifdef APP-PLUS || H5
     this.videoContext = uni.createVideoContext('fullScreenVideo')
     // #endif
     // #ifdef MP-WEIXIN
     this.videoContext = uni.createVideoContext('fullScreenVideo', this)
     // #endif
    },
    beforeDestroy() {clearTimeout(this.timer)
      this.timer = null
   },  
   methods: {toPlayVideo(url) {this.videoUrl = url //.split('?')[0]
        this.timer = setTimeout(() =>{
          // #ifdef APP-PLUS || H5
          this.videoContext = uni.createVideoContext('fullScreenVideo')
          // #endif
          // #ifdef MP-WEIXIN
          this.videoContext = uni.createVideoContext('fullScreenVideo', this)
          // #endif
         // 微信开发工具不失效,要真机测试
          this.videoContext.requestFullScreen({direction: 0})          
          this.videoContext.play()}, 100)
      },
      fullscreenchange(e) {if(!e.detail.fullScreen) {this.videoContext.stop()
          this.videoUrl = false
        }
      },
      onVideoError(err) {console.log('播放视频失败:', err)
      },
      deleteFile(file, index) {
        uni.showModal({
            title: '提醒',
            content: '您确定要删除此项吗?',
            success: res => {if (res.confirm) {this.$emit('delete-item', { file, index})
            }
          }
        })
      },
    }

2、获取视频的封面

展现视频的 view 外面,要用一个图片作为视频的封面,能够用视频的首帧。
获取视频的封面,用到 renderjs,通过 canvas 实现。

html 局部

<!-- #ifdef APP-VUE || H5 -->
<view :prop="videos" :change:prop="rdVideo.getAllPoster" style="opacity: 0;"></view>
<!-- #endif -->

js 局部

<!-- #ifdef APP-VUE || H5 -->
<script module="rdVideo" lang="renderjs">
  import mixinVideo from './video-poster.js'
  // 监听 videos, 有更改调用 rdVideo.getAllPoster 办法获取视频封面
  export default {mixins: [mixinVideo]
  }  
</script>

<script>
export default {data() {
    return {videos: []
    } 
  }
  methods: {
    // 获取视频封面后,调用此办法更新视频数据
    updateVideos(newVal) {this.videos = newVal},
  }
}
</script>
<!-- #endif -->

video-poster.js

const mixinVideo = {
  methods: {getVideoPoster(url) {return new Promise(function (resolve, reject) {let video = document.createElement('video')
        video.setAttribute('crossOrigin', 'anonymous') // 解决跨域,H5 需后盾反对,申请的视频资源响应招标需有 Access-Control-Allow-Origin
        video.setAttribute('src', url)
        video.setAttribute('width', 400)
        video.setAttribute('height', 400)
        video.setAttribute('preload', 'auto')
        // uni.chooseVideo 抉择视频,入选用手机拍摄的视频时,地址是绝对地址,如 _doc/uniapp_temp_1650594368317/camera/1650594390147.mp4
        // 可播放,然而 loadeddata 始终不执行,会触发 error 事件,视频加载失败
        // 因先转换老本地地址
        video.addEventListener('loadeddata', function () {console.log('视频第一帧加载完')
          let canvas = document.createElement('canvas')
          let width = video.width // canvas 的尺寸和图片一样
          let height = video.height
          canvas.width = width
          canvas.height = height
          canvas.getContext('2d').drawImage(video, 0, 0, width, height) // 绘制 canvas
          const dataURL = canvas.toDataURL('image/jpeg') // 转换为 base64
          console.log('getVideoPoster-dataURL', dataURL.slice(0, 16)) 
          resolve(dataURL)
        })
        
        video.addEventListener('error', err => {console.log('视频加载失败', err)         
          reject(err)
        })
      })
    },
    async getAllPoster(newVal, oldVal, owner, instance) {console.log('执行 getAllPoster')
      // renderjs 中,监听的属性 videos 是一个数组,寄存的是选取的视频信息     
      // 删除,或 updateVideos 更新后(长度一样)if(newVal.length <= oldVal.length) return
      // 有默认值或增加时
      const newList = []
      for(const item of newVal) {
        // 已获取视频封面的不再反复获取
        if(item.posterUrl !== undefined) {newList.push({ ...item})
          continue
        }
  
        try {
          let url = item.url
          // 拍摄视频:_doc/uniapp_temp_1650594368317/camera/1650594390147.mp4
          // 网络视频:https://
          // 本地视频:file://
          if(!item.url.includes('file://') && !item.url.includes('https://')) {
            // 将本地 URL 门路转换成平台绝对路径
            // 如输出 url 为“_doc/a.png”:// Android 平台转换后的门路为“/storage/sdcard0/Android/data/io.dcloud.HBuilder/.HBuilder/apps/HBuilder/doc/a.png”;// #ifdef APP-VUE
            url =  'file://' + plus.io.convertLocalFileSystemURL(item.url)
            // #endif   
          }
          const dataUrl = await this.getVideoPoster(url)
          newList.push({...item, posterUrl: dataUrl})
        } catch (err) {newList.push({ ...item})
        }
      }
      this.$ownerInstance.callMethod('updateVideos', newList)
    }
  }
}
  
export default mixinVideo
退出移动版