uniapp 使用renderjs 封装 video-player 视频播放器, html5视频播放器-解决视频层级、覆盖、播放卡顿

uniApp 项目中,uniapp 提供的的 video 原生视频组件层级太高,难以遮挡;该视频播放器可以被其他元素进行覆盖、遮挡,解决video原生视频播放视频会出现卡顿问题。

代码示例

javascript 复制代码
<template>
  <view
    class="player-wrapper"
    :id="videoWrapperId"
    :parentId="id"
    :randomNum="randomNum"
    :change:randomNum="domVideoPlayer.randomNumChange"
    :viewportProps="viewportProps"
    :change:viewportProps="domVideoPlayer.viewportChange"
    :videoSrc="videoSrc"
    :change:videoSrc="domVideoPlayer.initVideoPlayer"
    :command="eventCommand"
    :change:command="domVideoPlayer.triggerCommand"
    :func="renderFunc"
    :change:func="domVideoPlayer.triggerFunc"
  />
</template>

<script>
export default {
  props: {
    src: {
      type: String,
      default: '',
    },
    autoplay: {
      type: Boolean,
      default: false,
    },
    loop: {
      type: Boolean,
      default: false,
    },
    controls: {
      type: Boolean,
      default: false,
    },
    objectFit: {
      type: String,
      default: 'contain',
    },
    muted: {
      type: Boolean,
      default: false,
    },
    playbackRate: {
      type: Number,
      default: 1,
    },
    isLoading: {
      type: Boolean,
      default: false,
    },
    poster: {
      type: String,
      default: '',
    },
    id: {
      type: String,
      default: '',
    },
  },
  data() {
    return {
      randomNum: Math.floor(Math.random() * 100000000),
      videoSrc: '',
      // 父组件向子组件传递的事件指令(video的原生事件)
      eventCommand: null,
      // 父组件传递过来的,对 renderjs 层的函数执行(对视频控制的自定义事件)
      renderFunc: {
        name: null,
        params: null,
      },
      // 提供给父组件进行获取的视频属性
      currentTime: 0,
      duration: 0,
      playing: false,
    }
  },
  watch: {
    // 监听视频资源地址更新
    src: {
      handler(val) {
        if (!val) return
        setTimeout(() => {
          this.videoSrc = val
        }, 0)
      },
      immediate: true,
    },
  },
  computed: {
    videoWrapperId() {
      return `video-wrapper-${this.randomNum}`
    },
    // 聚合视图层的所有数据变化,传给renderjs的渲染层
    viewportProps() {
      return {
        autoplay: this.autoplay,
        muted: this.muted,
        controls: this.controls,
        loop: this.loop,
        objectFit: this.objectFit,
        poster: this.poster,
        isLoading: this.isLoading,
        playbackRate: this.playbackRate,
      }
    },
  },
  // 方法
  methods: {
    // 传递事件指令给父组件
    eventEmit({ event, data }) {
      this.$emit(event, data)
    },
    // 修改view视图层的data数据
    setViewData({ key, value }) {
      key && this.$set(this, key, value)
    },
    // 重置事件指令
    resetEventCommand() {
      this.eventCommand = null
    },
    // 播放指令
    play() {
      this.eventCommand = 'play'
    },
    // 暂停指令
    pause() {
      this.eventCommand = 'pause'
    },
    // 重置自定义函数指令
    resetFunc() {
      this.renderFunc = {
        name: null,
        params: null,
      }
    },
    // 自定义函数 - 移除视频
    remove(params) {
      this.renderFunc = {
        name: 'removeHandler',
        params,
      }
    },
    // 自定义函数 - 全屏播放
    fullScreen(params) {
      this.renderFunc = {
        name: 'fullScreenHandler',
        params,
      }
    },
    // 自定义函数 - 跳转到指定时间点
    toSeek(sec, isDelay = false) {
      this.renderFunc = {
        name: 'toSeekHandler',
        params: { sec, isDelay },
      }
    },
  },
}
</script>

<script module="domVideoPlayer" lang="renderjs">
const PLAYER_ID = 'DOM_VIDEO_PLAYER'
export default {
  data() {
    return {
      num: '',
      videoEl: null,
      loadingEl: null,
      // 延迟生效的函数
      delayFunc: null,
      renderProps: {}
    }
  },
  computed: {
    playerId() {
      return `${PLAYER_ID}_${this.num}`
    },
    wrapperId() {
      return `video-wrapper-${this.num}`
    }
  },
  methods: {
    isApple() {
      const ua = navigator.userAgent.toLowerCase()
      return ua.indexOf('iphone') !== -1 || ua.indexOf('ipad') !== -1
    },
    async initVideoPlayer(src) {
      this.delayFunc = null
      await this.$nextTick()
      if (!src) return
      if (this.videoEl) {
        // 切换视频源
        if (!this.isApple() && this.loadingEl) {
          this.loadingEl.style.display = 'block'
        }
        this.videoEl.src = src
        return
      }

      const videoEl = document.createElement('video')
      this.videoEl = videoEl
      // 开始监听视频相关事件
      this.listenVideoEvent()

      const { autoplay, muted, controls, loop, playbackRate, objectFit, poster } = this.renderProps
      videoEl.src = src
      videoEl.autoplay = autoplay
      videoEl.controls = controls
      videoEl.loop = loop
      videoEl.muted = muted
      videoEl.playbackRate = playbackRate
      videoEl.id = this.playerId
      // videoEl.setAttribute('x5-video-player-type', 'h5')
      videoEl.setAttribute('preload', 'auto')
      videoEl.setAttribute('playsinline', true)
      videoEl.setAttribute('webkit-playsinline', true)
      videoEl.setAttribute('crossorigin', 'anonymous')
      videoEl.setAttribute('controlslist', 'nodownload')
      videoEl.setAttribute('disablePictureInPicture', true)
      videoEl.style.objectFit = objectFit
      poster && (videoEl.poster = poster)
      videoEl.style.width = '100%'
      videoEl.style.height = '100%'

      // 插入视频元素
      // document.getElementById(this.wrapperId).appendChild(videoEl)
      const playerWrapper = document.getElementById(this.wrapperId)
      playerWrapper.insertBefore(videoEl, playerWrapper.firstChild)

      // 插入loading 元素(遮挡安卓的默认加载过程中的黑色播放按钮)
      this.createLoading()
    },
    // 创建 loading
    createLoading() {
      const { isLoading } = this.renderProps
      if (!this.isApple() && isLoading) {
        const loadingEl = document.createElement('div')
        this.loadingEl = loadingEl
        loadingEl.className = 'loading-wrapper'
        loadingEl.style.position = 'absolute'
        loadingEl.style.top = '0'
        loadingEl.style.left = '0'
        loadingEl.style.zIndex = '1'
        loadingEl.style.width = '100%'
        loadingEl.style.height = '100%'
        loadingEl.style.backgroundColor = 'black'
        document.getElementById(this.wrapperId).appendChild(loadingEl)

        // 创建 loading 动画
        const animationEl = document.createElement('div')
        animationEl.className = 'loading'
        animationEl.style.zIndex = '2'
        animationEl.style.position = 'absolute'
        animationEl.style.top = '50%'
        animationEl.style.left = '50%'
        animationEl.style.marginTop = '-15px'
        animationEl.style.marginLeft = '-15px'
        animationEl.style.width = '30px'
        animationEl.style.height = '30px'
        animationEl.style.border = '2px solid #FFF'
        animationEl.style.borderTopColor = 'rgba(255, 255, 255, 0.2)'
        animationEl.style.borderRightColor = 'rgba(255, 255, 255, 0.2)'
        animationEl.style.borderBottomColor = 'rgba(255, 255, 255, 0.2)'
        animationEl.style.borderRadius = '100%'
        animationEl.style.animation = 'circle infinite 0.75s linear'
        loadingEl.appendChild(animationEl)

        // 创建 loading 动画所需的 keyframes
        const style = document.createElement('style')
        const keyframes = `
          @keyframes circle {
            0% {
              transform: rotate(0);
            }
            100% {
              transform: rotate(360deg);
            }
          }
        `
        style.type = 'text/css'
        if (style.styleSheet) {
          style.styleSheet.cssText = keyframes
        } else {
          style.appendChild(document.createTextNode(keyframes))
        }
        document.head.appendChild(style)
      }
    },
    // 监听视频相关事件
    listenVideoEvent() {
      // 播放事件监听
      const playHandler = () => {
        this.$ownerInstance.callMethod('eventEmit', { event: 'play' })
        this.$ownerInstance.callMethod('setViewData', {
          key: 'playing',
          value: true
        })

        if (this.loadingEl) {
          this.loadingEl.style.display = 'none'
        }
      }
      this.videoEl.removeEventListener('play', playHandler)
      this.videoEl.addEventListener('play', playHandler)

      // 暂停事件监听
      const pauseHandler = () => {
        this.$ownerInstance.callMethod('eventEmit', { event: 'pause' })
        this.$ownerInstance.callMethod('setViewData', {
          key: 'playing',
          value: false
        })
      }
      this.videoEl.removeEventListener('pause', pauseHandler)
      this.videoEl.addEventListener('pause', pauseHandler)

      // 结束事件监听
      const endedHandler = () => {
        this.$ownerInstance.callMethod('eventEmit', { event: 'ended' })
        this.$ownerInstance.callMethod('resetEventCommand')
      }
      this.videoEl.removeEventListener('ended', endedHandler)
      this.videoEl.addEventListener('ended', endedHandler)

      // 加载完成事件监听
      const canPlayHandler = () => {
        this.$ownerInstance.callMethod('eventEmit', { event: 'canplay' })
        this.execDelayFunc()
      }
      this.videoEl.removeEventListener('canplay', canPlayHandler)
      this.videoEl.addEventListener('canplay', canPlayHandler)

      // 加载失败事件监听
      const errorHandler = (e) => {
        if (this.loadingEl) {
          this.loadingEl.style.display = 'block'
        }
        this.$ownerInstance.callMethod('eventEmit', { event: 'error' })
      }
      this.videoEl.removeEventListener('error', errorHandler)
      this.videoEl.addEventListener('error', errorHandler)

      // loadedmetadata 事件监听
      const loadedMetadataHandler = () => {
        this.$ownerInstance.callMethod('eventEmit', { event: 'loadedmetadata' })
        // 获取视频的长度
        const duration = this.videoEl.duration
        this.$ownerInstance.callMethod('eventEmit', {
          event: 'durationchange',
          data: duration
        })

        this.$ownerInstance.callMethod('setViewData', {
          key: 'duration',
          value: duration
        })

        // 加载首帧视频 模拟出封面图
        this.loadFirstFrame()
      }
      this.videoEl.removeEventListener('loadedmetadata', loadedMetadataHandler)
      this.videoEl.addEventListener('loadedmetadata', loadedMetadataHandler)

      // 播放进度监听
      const timeupdateHandler = (e) => {
        const currentTime = e.target.currentTime
        this.$ownerInstance.callMethod('eventEmit', {
          event: 'timeupdate',
          data: currentTime
        })

        this.$ownerInstance.callMethod('setViewData', {
          key: 'currentTime',
          value: currentTime
        })

      }
      this.videoEl.removeEventListener('timeupdate', timeupdateHandler)
      this.videoEl.addEventListener('timeupdate', timeupdateHandler)

      // 倍速播放监听
      const ratechangeHandler = (e) => {
        const playbackRate = e.target.playbackRate
        this.$ownerInstance.callMethod('eventEmit', {
          event: 'ratechange',
          data: playbackRate
        })
      }
      this.videoEl.removeEventListener('ratechange', ratechangeHandler)
      this.videoEl.addEventListener('ratechange', ratechangeHandler)

      // 全屏事件监听
      if (this.isApple()) {
        const webkitbeginfullscreenHandler = () => {
          const presentationMode = this.videoEl.webkitPresentationMode
          let isFullScreen = null
          if (presentationMode === 'fullscreen') {
            isFullScreen = true
          } else {
            isFullScreen = false
          }
          this.$ownerInstance.callMethod('eventEmit', {
            event: 'fullscreenchange',
            data: isFullScreen
          })
        }
        this.videoEl.removeEventListener('webkitpresentationmodechanged', webkitbeginfullscreenHandler)
        this.videoEl.addEventListener('webkitpresentationmodechanged', webkitbeginfullscreenHandler)
      } else {
        const fullscreenchangeHandler = () => {
          let isFullScreen = null
          if (document.fullscreenElement) {
            isFullScreen = true
          } else {
            isFullScreen = false
          }
          this.$ownerInstance.callMethod('eventEmit', {
            event: 'fullscreenchange',
            data: isFullScreen
          })
        }
        document.removeEventListener('fullscreenchange', fullscreenchangeHandler)
        document.addEventListener('fullscreenchange', fullscreenchangeHandler)
      }
    },
    // 加载首帧视频,模拟出封面图
    loadFirstFrame() {
      let { autoplay, muted } = this.renderProps
      if (this.isApple()) {
        this.videoEl.play()
        if (!autoplay) {
          this.videoEl.pause()
        }
      } else {
        // optimize: timeout 延迟调用是为了规避控制台的`https://goo.gl/LdLk22`这个报错
        /**
         * 原因:chromium 内核中,谷歌协议规定,视频不允许在非静音状态下进行自动播放
         * 解决:在自动播放时,先将视频静音,然后延迟调用 play 方法,播放视频
         * 说明:iOS 的 Safari 内核不会有这个,仅在 Android 设备出现,即使有这个报错也不影响的,所以不介意控制台报错的话是可以删掉这个 timeout 的
         */
        this.videoEl.muted = true
        setTimeout(() => {
          this.videoEl.play()
          this.videoEl.muted = muted
          if (!autoplay) {
            setTimeout(() => {
              this.videoEl.pause()
            }, 100)
          }
        }, 10)
      }
    },
    triggerCommand(eventType) {
      if (eventType) {
        this.$ownerInstance.callMethod('resetEventCommand')
        this.videoEl && this.videoEl[eventType]()
      }
    },
    triggerFunc(func) {
      const { name, params } = func || {}
      if (name) {
        this[name](params)
        this.$ownerInstance.callMethod('resetFunc')
      }
    },
    removeHandler() {
      if (this.videoEl) {
        this.videoEl.pause()
        this.videoEl.src = ''
        this.$ownerInstance.callMethod('setViewData', {
          key: 'videoSrc',
          value: ''
        })
        this.videoEl.load()
      }
    },
    fullScreenHandler() {
      if (this.isApple()) {
        this.videoEl.webkitEnterFullscreen()
      } else {
        this.videoEl.requestFullscreen()
      }
    },
    toSeekHandler({ sec, isDelay }) {
      const func = () => {
        if (this.videoEl) {
          this.videoEl.currentTime = sec
        }
      }

      // 延迟执行
      if (isDelay) {
        this.delayFunc = func
      } else {
        func()
      }
    },
    // 执行延迟函数
    execDelayFunc() {
      this.delayFunc && this.delayFunc()
      this.delayFunc = null
    },
    viewportChange(props) {
      this.renderProps = props
      const { autoplay, muted, controls, loop, playbackRate } = props
      if (this.videoEl) {
        this.videoEl.autoplay = autoplay
        this.videoEl.controls = controls
        this.videoEl.loop = loop
        this.videoEl.muted = muted
        this.videoEl.playbackRate = playbackRate
      }
    },
    randomNumChange(val) {
      this.num = val
    }
  }
}
</script>

<style scoped>
.player-wrapper {
  overflow: hidden;
  height: 100%;
  padding: 0;
  position: relative;
}
</style>

使用示例

javascript 复制代码
<template>
  <view>
    <view style="width: 750rpx">
      <DomVideoPlayer
        ref="domVideoPlayer"
        object-fit="contain"
        :controls="controls"
        :autoplay="autoplay"
        :loop="loop"
        :src="src"
        :playback-rate="playbackRate"
        @play="onPlay"
        @pause="onPause"
        @ended="onEnded"
        @durationchange="onDurationChange"
        @timeupdate="onTimeUpdate"
        @ratechange="onRateChange"
        @fullscreenchange="onFullscreenChange"
      />
    </view>

    <!-- video的属性值 -->
    <view class="action-box">
      <view>播放进度: {{ progress }}</view>
      <view>播放时间: {{ showPlayTime }}</view>
      <view>当前时长: {{ currentTime }}</view>
      <view>总时长: {{ duration }}</view>
      <view>播放倍速: {{ playbackRate }}</view>
      <view>播放控制器: {{ controls }}</view>
      <view>循环播放: {{ loop }}</view>
      <view>自动播放: {{ autoplay }}</view>
    </view>

    <!-- 操作 -->
    <view class="action-box">
      <h3>事件调用</h3>
      <!-- 单个按钮控制播放/暂停 -->
      <button @tap="doPlaying">
        单个按钮控制:
        <text v-if="!playing">播放</text>
        <text v-else>暂停</text>
      </button>
      <!-- 分别控制播放/暂停 -->
      <button @tap="doPlay">播放</button>
      <button @tap="doPause">暂停</button>
    </view>

    <!-- 属性操作 -->
    <view class="action-box">
      <h3>更改属性</h3>
      <button @tap="switchRate">切换到{{ playbackRate === 1 ? 2 : 1 }}倍速播放</button>
      <button @tap="switchControls">切换视频控制栏:{{ !controls ? '显示' : '隐藏' }}</button>
    </view>

    <!-- 自定义操作 -->
    <view class="action-box">
      <h3>自定义事件</h3>
      <button @tap="doSeek(-15)">快退15秒</button>
      <button @tap="doSeek(15)">快进15秒</button>
      <button @tap="doFullScreen">全屏播放</button>
      <button @tap="doRemove">移除视频</button>
      <button @tap="doUpdateSrc">更换src</button>
    </view>
  </view>
</template>

<script setup>
import { ref, computed } from 'vue'
import DomVideoPlayer from './DomVideoPlayer.vue'
// 将xx秒转为 xx:xx 分秒格式
const formatSec2Time = (time) => {
  const min = Math.floor(time / 60)
  const sec = Math.floor(time % 60)
  return `${min}:${sec < 10 ? '0' + sec : sec}`
}

const src = ref(
  'https://env-00jxt6hwsqjo.normal.cloudstatic.cn/2023%E5%93%81%E7%89%8C%E5%AE%A3%E4%BC%A0%E7%89%87.mp4',
)
const playing = ref(false)
const loop = ref(false)
const controls = ref(true)
const autoplay = ref(false)
const playbackRate = ref(1)
const currentTime = ref(0)
const duration = ref(0)
const domVideoPlayer = ref(null)

const progress = computed(() => {
  const percent = (currentTime.value / duration.value) * 100
  return percent.toFixed(2) + '%'
})

const showPlayTime = computed(() => {
  const curr = formatSec2Time(currentTime.value)
  const dur = formatSec2Time(duration.value)
  return `${curr} / ${dur}`
})

const onPlay = () => {
  console.log('onPlay')
  playing.value = true
}

const onPause = () => {
  console.log('onPause')
  playing.value = false
}

const onEnded = () => {
  console.log('onEnded')
  playing.value = false
}

const onDurationChange = (e) => {
  console.log('onDurationChange', e)
  duration.value = e
}

const onTimeUpdate = (e) => {
  currentTime.value = e
}

const onRateChange = (e) => {
  console.log('onRateChange', e)
  playbackRate.value = e
}

const onFullscreenChange = (e) => {
  console.log('onFullScreenChange', e)
}

const doPlaying = () => {
  if (domVideoPlayer.value.playing) {
    doPause()
  } else {
    doPlay()
  }
}

const doPlay = () => {
  domVideoPlayer.value.play()
}

const doPause = () => {
  domVideoPlayer.value.pause()
}

const doSeek = (time) => {
  time += domVideoPlayer.value.currentTime
  domVideoPlayer.value.toSeek(time)
}

const doFullScreen = () => {
  domVideoPlayer.value.fullScreen()
}

const doRemove = () => {
  src.value = ''
  domVideoPlayer.value.remove()
}

const doUpdateSrc = () => {
  src.value =
    'https://env-00jxt6hwsqjo.normal.cloudstatic.cn/2023%E5%93%81%E7%89%8C%E5%AE%A3%E4%BC%A0%E7%89%87.mp4'
}

const switchRate = () => {
  playbackRate.value = playbackRate.value === 1 ? 2 : 1
}

const switchControls = () => {
  controls.value = !controls.value
}
</script>

<style scoped>
.action-box {
  margin-top: 30rpx;
  padding: 0 60rpx;
}

.action-box button {
  margin-top: 10rpx;
}
</style>
相关推荐
钱端工程师2 小时前
uniapp封装uni.request请求,实现重复接口请求中断上次请求(防抖)
前端·javascript·uni-app
茶憶2 小时前
uni-app app移动端实现纵向滑块功能,并伴随自动播放
javascript·vue.js·uni-app·html·scss
dcloud_jibinbin2 小时前
【uniapp】解决小程序分包下的json文件编译后生成到主包的问题
前端·性能优化·微信小程序·uni-app·vue·json
茶憶2 小时前
uniapp移动端实现触摸滑动功能:上下滑动展开收起内容,左右滑动删除列表
前端·javascript·vue.js·uni-app
蒲公英源码2 小时前
uniapp开源ERP多仓库管理系统
mysql·elementui·uni-app·php
shykevin2 小时前
uni-app x开发商城系统,小程序发布,h5发布,安卓打包
android·小程序·uni-app
且白2 小时前
uniapp接入安卓端极光推送离线打包
android·uni-app
Ayn慢慢2 小时前
uni-app PDA焦点录入实现
前端·javascript·uni-app
骄傲的心别枯萎7 小时前
RV1126 NO.37:OPENCV的图像叠加功能
人工智能·opencv·计算机视觉·音视频·视频编解码·rv1126