uniApp 项目中,uniapp 提供的的 video 原生视频组件层级太高,难以遮挡;该视频播放器可以被其他元素进行覆盖、遮挡,解决video原生视频播放视频会出现卡顿问题。
代码示例
<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: '',
eventCommand: null,
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}`
},
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)
},
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('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%'
const playerWrapper = document.getElementById(this.wrapperId)
playerWrapper.insertBefore(videoEl, playerWrapper.firstChild)
this.createLoading()
},
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)
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)
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)
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 {
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>
使用示例
<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'
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>