|
@@ -0,0 +1,252 @@
|
|
|
+import { defineComponent, nextTick, onMounted, reactive, toRefs, watch, ref } from 'vue'
|
|
|
+import 'plyr/dist/plyr.css'
|
|
|
+import Plyr from 'plyr'
|
|
|
+import styles from './index.module.less'
|
|
|
+import { iconVideoBg, iconLoop, iconLoopActive, iconPlay, iconPause } from '../../image/icons.json'
|
|
|
+
|
|
|
+export default defineComponent({
|
|
|
+ name: 'video-play',
|
|
|
+ props: {
|
|
|
+ item: {
|
|
|
+ type: Object,
|
|
|
+ default: () => {
|
|
|
+ return {}
|
|
|
+ }
|
|
|
+ },
|
|
|
+ activeModel: {
|
|
|
+ type: Boolean,
|
|
|
+ default: true
|
|
|
+ }
|
|
|
+ },
|
|
|
+ emits: ['play', 'pause', 'ended', 'close', 'seeked', 'seeking', 'waiting', 'timeupdate'],
|
|
|
+ setup(props, { emit, expose }) {
|
|
|
+ const { item } = toRefs(props)
|
|
|
+ const data = reactive({
|
|
|
+ videoContianerRef: null as unknown as HTMLAudioElement,
|
|
|
+ videoState: 'pause' as 'init' | 'play' | 'pause',
|
|
|
+ animationState: 'start' as 'start' | 'end',
|
|
|
+ videoItem: null as unknown as Plyr
|
|
|
+ })
|
|
|
+ const controlID = 'v' + Date.now() + Math.floor(Math.random() * 100)
|
|
|
+ const playBtnId = 'play' + Date.now() + Math.floor(Math.random() * 100)
|
|
|
+ const loopBtnId = 'loop' + Date.now() + Math.floor(Math.random() * 100)
|
|
|
+
|
|
|
+ const togglePlay = (e: Event) => {
|
|
|
+ e.stopPropagation()
|
|
|
+ if (!data.videoContianerRef.paused) {
|
|
|
+ data.videoItem?.pause()
|
|
|
+ } else {
|
|
|
+ data.videoContianerRef?.play()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const toggleLoop = () => {
|
|
|
+ const loopBtn = document.getElementById(loopBtnId)
|
|
|
+ if (!loopBtn || !data.videoItem) return
|
|
|
+ const isLoop = data.videoItem.loop
|
|
|
+ if (isLoop) {
|
|
|
+ loopBtn.classList.remove(styles.active)
|
|
|
+ } else {
|
|
|
+ loopBtn.classList.add(styles.active)
|
|
|
+ }
|
|
|
+ data.videoItem.loop = !data.videoItem.loop
|
|
|
+ }
|
|
|
+ const onDefault = () => {
|
|
|
+ document.getElementById(controlID)?.addEventListener('click', (e: Event) => {
|
|
|
+ e.stopPropagation()
|
|
|
+ if (data.videoContianerRef.paused) return
|
|
|
+ emit('close')
|
|
|
+ })
|
|
|
+ document.getElementById(controlID)?.addEventListener('touchmove', () => {
|
|
|
+ if (data.videoContianerRef.paused) return
|
|
|
+ emit('close')
|
|
|
+ })
|
|
|
+ document.getElementById(playBtnId)?.addEventListener('click', togglePlay)
|
|
|
+ document.getElementById(loopBtnId)?.addEventListener('click', toggleLoop)
|
|
|
+ setName()
|
|
|
+ }
|
|
|
+ const setName = () => {
|
|
|
+ const nameEl = document.getElementById('videoItemName')
|
|
|
+ if (nameEl) {
|
|
|
+ nameEl.innerHTML = item.value.name || ''
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const changePlayBtn = (code: string) => {
|
|
|
+ const playBtn = document.getElementById(playBtnId)
|
|
|
+ if (!playBtn) return
|
|
|
+ if (code == 'play') {
|
|
|
+ playBtn.classList.remove(styles.btnPause)
|
|
|
+ playBtn.classList.add(styles.btnPlay)
|
|
|
+ } else {
|
|
|
+ playBtn.classList.remove(styles.btnPlay)
|
|
|
+ playBtn.classList.add(styles.btnPause)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const controls = `
|
|
|
+ <div id="${controlID}" class="plyr__controls bottomFixed ${styles.controls}">
|
|
|
+ <div class="${styles.time}">
|
|
|
+ <div class="plyr__time plyr__time--current" aria-label="Current time">00:00</div>
|
|
|
+ <div class="plyr__time plyr__time--duration" aria-label="Duration">00:00</div>
|
|
|
+ </div>
|
|
|
+ <div class="${styles.slider}">
|
|
|
+ <div class="plyr__progress">
|
|
|
+ <input data-plyr="seek" type="range" min="0" max="100" step="0.01" value="0" aria-label="Seek">
|
|
|
+ <progress class="plyr__progress__buffer" min="0" max="100" value="0">% buffered</progress>
|
|
|
+ <span role="tooltip" class="plyr__tooltip">00:00</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="${styles.actions}">
|
|
|
+ <div class="${styles.actionWrap}">
|
|
|
+ <button id="${playBtnId}" class="${styles.actionBtn}">
|
|
|
+ <div class="van-loading van-loading--circular" aria-live="polite" aria-busy="true"><span class="van-loading__spinner van-loading__spinner--circular" style="color: rgb(255, 255, 255);"><svg class="van-loading__circular" viewBox="25 25 50 50"><circle cx="50" cy="50" r="20" fill="none"></circle></svg></span></div>
|
|
|
+ <img class="${styles.playIcon}" src="${iconPlay}" />
|
|
|
+ <img class="${styles.playIcon}" src="${iconPause}" />
|
|
|
+ </button>
|
|
|
+ <button id="${loopBtnId}" class="${styles.actionBtn} ${styles.loopBtn}">
|
|
|
+ <img class="loop" src="${iconLoop}" />
|
|
|
+ <img class="loopActive" src="${iconLoopActive}" />
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <div id="videoItemName"></div>
|
|
|
+ </div>
|
|
|
+ </div>`
|
|
|
+
|
|
|
+ onMounted(() => {
|
|
|
+ data.videoItem = new Plyr(data.videoContianerRef, {
|
|
|
+ autoplay: true,
|
|
|
+ controls: controls,
|
|
|
+ ratio: '16:9', // 强制所有视频的纵横比
|
|
|
+ hideControls: false, // 在 2 秒没有鼠标或焦点移动、控制元素模糊(制表符退出)、播放开始或进入全屏时自动隐藏视频控件。只要移动鼠标、聚焦控制元素或暂停播放,控件就会立即重新出现。
|
|
|
+ clickToPlay: false, // 单击(或点击)视频容器将切换播放/暂停
|
|
|
+ fullscreen: { enabled: false, fallback: false, iosNative: false } // 不适用全屏
|
|
|
+ })
|
|
|
+
|
|
|
+ nextTick(() => {
|
|
|
+ onDefault()
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ const toggleHideControl = (isShow: boolean) => {
|
|
|
+ data.videoItem?.toggleControls(isShow)
|
|
|
+ }
|
|
|
+ watch(
|
|
|
+ () => props.activeModel,
|
|
|
+ () => {
|
|
|
+ toggleHideControl(props.activeModel)
|
|
|
+ }
|
|
|
+ )
|
|
|
+
|
|
|
+ watch(
|
|
|
+ () => props.item,
|
|
|
+ () => {
|
|
|
+ setName()
|
|
|
+ }
|
|
|
+ )
|
|
|
+ let videoTimer = null as any
|
|
|
+ let videoTimerErrorCount = 0
|
|
|
+ const handlePlayVideo = () => {
|
|
|
+ // if (videoTimerErrorCount > 5) {
|
|
|
+ // return
|
|
|
+ // }
|
|
|
+ clearTimeout(videoTimer)
|
|
|
+ nextTick(() => {
|
|
|
+ data.videoContianerRef.play().catch((err) => {
|
|
|
+ // console.log('🚀 ~ err:', err)
|
|
|
+ videoTimer = setTimeout(() => {
|
|
|
+ if (err?.message?.includes('play()')) {
|
|
|
+ emit('play')
|
|
|
+ }
|
|
|
+ handlePlayVideo()
|
|
|
+ }, 1000)
|
|
|
+ })
|
|
|
+ })
|
|
|
+ videoTimerErrorCount++
|
|
|
+ }
|
|
|
+
|
|
|
+ let videoErrorTimer = null as any
|
|
|
+ let videoErrorCount = 0
|
|
|
+ const handleErrorVideo = () => {
|
|
|
+ if (videoErrorCount > 5) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ clearTimeout(videoErrorTimer)
|
|
|
+ nextTick(() => {
|
|
|
+ videoErrorTimer = setTimeout(() => {
|
|
|
+ data.videoContianerRef.src = props.item?.content
|
|
|
+ emit('play')
|
|
|
+ data.videoContianerRef.load()
|
|
|
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
|
+ handleErrorVideo()
|
|
|
+ }, 1000)
|
|
|
+ })
|
|
|
+ videoErrorCount++
|
|
|
+ }
|
|
|
+ const getVideoRef = () => {
|
|
|
+ return data.videoContianerRef
|
|
|
+ }
|
|
|
+
|
|
|
+ const getPlyrRef = () => {
|
|
|
+ return data.videoItem
|
|
|
+ }
|
|
|
+ expose({
|
|
|
+ getVideoRef,
|
|
|
+ getPlyrRef
|
|
|
+ })
|
|
|
+
|
|
|
+ return () => (
|
|
|
+ <div class={styles.videoWrap}>
|
|
|
+ <video
|
|
|
+ ref={(el) => (data.videoContianerRef = el as unknown as HTMLAudioElement)}
|
|
|
+ class={styles.itemDiv}
|
|
|
+ src={props.item?.content}
|
|
|
+ poster={iconVideoBg}
|
|
|
+ webkit-playsinline
|
|
|
+ playsinline
|
|
|
+ x5-video-player-type="h5"
|
|
|
+ onLoadedmetadata={() => {
|
|
|
+ data.videoState = 'pause'
|
|
|
+ changePlayBtn('play')
|
|
|
+ nextTick(() => {
|
|
|
+ data.videoContianerRef.currentTime = 0
|
|
|
+ nextTick(handlePlayVideo)
|
|
|
+ })
|
|
|
+ }}
|
|
|
+ onPlay={() => {
|
|
|
+ videoErrorCount = 0
|
|
|
+ console.log('开始播放')
|
|
|
+ data.videoState = 'play'
|
|
|
+ changePlayBtn('pause')
|
|
|
+ emit('close')
|
|
|
+ emit('play')
|
|
|
+ clearTimeout(videoErrorTimer)
|
|
|
+ }}
|
|
|
+ onPause={() => {
|
|
|
+ console.log('暂停播放')
|
|
|
+ data.videoState = 'pause'
|
|
|
+ changePlayBtn('play')
|
|
|
+ emit('pause')
|
|
|
+ }}
|
|
|
+ onEnded={() => {
|
|
|
+ console.log('播放结束')
|
|
|
+ data.videoState = 'pause'
|
|
|
+ changePlayBtn('play')
|
|
|
+ emit('ended')
|
|
|
+ }}
|
|
|
+ onSeeked={() => {
|
|
|
+ emit('seeked')
|
|
|
+ }}
|
|
|
+ onSeeking={() => {
|
|
|
+ emit('seeking')
|
|
|
+ }}
|
|
|
+ onTimeupdate={() => {
|
|
|
+ emit('timeupdate')
|
|
|
+ }}
|
|
|
+ onWaiting={() => {
|
|
|
+ emit('waiting')
|
|
|
+ }}
|
|
|
+ onError={handleErrorVideo}
|
|
|
+ ></video>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+ }
|
|
|
+})
|