|
@@ -1,4 +1,4 @@
|
|
|
-import { defineComponent, nextTick, onMounted, toRefs, watch } from 'vue'
|
|
|
+import { defineComponent, nextTick, onMounted, reactive, toRefs, watch } from 'vue'
|
|
|
import 'plyr/dist/plyr.css'
|
|
|
import Plyr from 'plyr'
|
|
|
import { ref } from 'vue'
|
|
@@ -9,6 +9,26 @@ import iconLoopActive from '../image/icon-loop-active.svg'
|
|
|
import iconplay from '../image/icon-play.svg'
|
|
|
import iconpause from '../image/icon-pause.svg'
|
|
|
|
|
|
+import TCPlayer from 'tcplayer.js'
|
|
|
+import 'tcplayer.js/dist/tcplayer.min.css'
|
|
|
+import { Slider } from 'vant'
|
|
|
+
|
|
|
+// 秒转分
|
|
|
+export const getSecondRPM = (second: number, type?: string) => {
|
|
|
+ if (isNaN(second)) return '00:00'
|
|
|
+ const mm = Math.floor(second / 60)
|
|
|
+ .toString()
|
|
|
+ .padStart(2, '0')
|
|
|
+ const dd = Math.floor(second % 60)
|
|
|
+ .toString()
|
|
|
+ .padStart(2, '0')
|
|
|
+ if (type === 'cn') {
|
|
|
+ return mm + '分' + dd + '秒'
|
|
|
+ } else {
|
|
|
+ return mm + ':' + dd
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
export default defineComponent({
|
|
|
name: 'video-play',
|
|
|
props: {
|
|
@@ -27,126 +47,135 @@ export default defineComponent({
|
|
|
default: false
|
|
|
}
|
|
|
},
|
|
|
- emits: ['loadedmetadata', 'togglePlay', 'ended', 'reset'],
|
|
|
+ emits: ['loadedmetadata', 'togglePlay', 'ended', 'reset', 'error', 'close'],
|
|
|
setup(props, { emit, expose }) {
|
|
|
const { item, isEmtry } = toRefs(props)
|
|
|
+ const data = reactive({
|
|
|
+ timer: null as any,
|
|
|
+ currentTime: 0,
|
|
|
+ duration: 0.1,
|
|
|
+ loop: false,
|
|
|
+ playState: 'pause' as 'play' | 'pause',
|
|
|
+ vudio: null as any,
|
|
|
+ showBar: true
|
|
|
+ })
|
|
|
const videoRef = ref()
|
|
|
- const videoItem = ref<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 videoItem = ref()
|
|
|
+ const videoID = 'video' + Date.now() + Math.floor(Math.random() * 100)
|
|
|
const toggleHideControl = (isShow: false) => {
|
|
|
- videoItem.value?.toggleControls(isShow)
|
|
|
- }
|
|
|
- const togglePlay = (e: Event) => {
|
|
|
- e.stopPropagation()
|
|
|
- videoItem.value?.togglePlay()
|
|
|
+ data.showBar = isShow
|
|
|
}
|
|
|
- const toggleLoop = (e: Event) => {
|
|
|
- const loopBtn = document.getElementById(loopBtnId)
|
|
|
- if (!loopBtn || !videoItem.value) return
|
|
|
- const isLoop = videoItem.value.loop
|
|
|
- if (isLoop) {
|
|
|
- loopBtn.classList.remove(styles.active)
|
|
|
+ // const togglePlay = (e: Event) => {
|
|
|
+ // e.stopPropagation()
|
|
|
+
|
|
|
+ // }
|
|
|
+ let playTimer = null as any
|
|
|
+ // 切换音频播放
|
|
|
+ const onToggleAudio = (state: 'play' | 'pause') => {
|
|
|
+ console.log(state, 'state')
|
|
|
+ clearTimeout(playTimer)
|
|
|
+ if (state === 'play') {
|
|
|
+ playTimer = setTimeout(() => {
|
|
|
+ videoItem.value?.play()
|
|
|
+ data.playState = 'play'
|
|
|
+ }, 100)
|
|
|
} else {
|
|
|
- loopBtn.classList.add(styles.active)
|
|
|
+ videoItem.value?.pause()
|
|
|
+ data.playState = 'pause'
|
|
|
}
|
|
|
- videoItem.value.loop = !videoItem.value.loop
|
|
|
+
|
|
|
+ emit('togglePlay', data.playState)
|
|
|
}
|
|
|
- const onDefault = () => {
|
|
|
- document.getElementById(controlID)?.addEventListener('click', (e: Event) => {
|
|
|
- e.stopPropagation()
|
|
|
- emit('reset')
|
|
|
- })
|
|
|
- document.getElementById(playBtnId)?.addEventListener('click', togglePlay)
|
|
|
- document.getElementById(loopBtnId)?.addEventListener('click', toggleLoop)
|
|
|
+ const toggleLoop = () => {
|
|
|
+ if (!videoItem.value) return
|
|
|
+ if (data.loop) {
|
|
|
+ videoItem.value.loop(false)
|
|
|
+ } else {
|
|
|
+ videoItem.value.loop(true)
|
|
|
+ }
|
|
|
+ data.loop = !data.loop
|
|
|
}
|
|
|
-
|
|
|
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)
|
|
|
+ data.playState = 'play'
|
|
|
} else {
|
|
|
- playBtn.classList.remove(styles.btnPlay)
|
|
|
- playBtn.classList.add(styles.btnPause)
|
|
|
+ data.playState = 'pause'
|
|
|
}
|
|
|
}
|
|
|
- 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>${item.value.name}</div>
|
|
|
- </div>
|
|
|
- </div>`
|
|
|
|
|
|
+ /** 改变播放时间 */
|
|
|
+ const handleChangeTime = (val: number) => {
|
|
|
+ data.currentTime = val
|
|
|
+ clearTimeout(data.timer)
|
|
|
+ data.timer = setTimeout(() => {
|
|
|
+ videoItem.value.currentTime(val)
|
|
|
+ data.timer = null
|
|
|
+ }, 300)
|
|
|
+ }
|
|
|
onMounted(() => {
|
|
|
- videoItem.value = new Plyr(videoRef.value, {
|
|
|
- autoplay: true,
|
|
|
- controls: controls,
|
|
|
- autopause: true, // 一次只允许
|
|
|
- ratio: '16:9', // 强制所有视频的纵横比
|
|
|
- hideControls: false, // 在 2 秒没有鼠标或焦点移动、控制元素模糊(制表符退出)、播放开始或进入全屏时自动隐藏视频控件。只要移动鼠标、聚焦控制元素或暂停播放,控件就会立即重新出现。
|
|
|
- clickToPlay: false, // 单击(或点击)视频容器将切换播放/暂停
|
|
|
- fullscreen: { enabled: false, fallback: false, iosNative: false } // 不适用全屏
|
|
|
- })
|
|
|
+ videoItem.value = TCPlayer(videoID, {
|
|
|
+ appID: '',
|
|
|
+ controls: false
|
|
|
+ // autoplay: true
|
|
|
+ }) // player-container-id 为播放器容器 ID,必须与 html 中一致
|
|
|
if (videoItem.value) {
|
|
|
+ videoItem.value.poster(props.item.coverImg) // 封面
|
|
|
+ videoItem.value.src(isEmtry.value ? '' : item.value.content) // url 播放地址
|
|
|
+
|
|
|
+ // 初步加载时
|
|
|
+ videoItem.value.one('loadedmetadata', (e: any) => {
|
|
|
+ // console.log(' Loading metadata')
|
|
|
+
|
|
|
+ // 获取时长
|
|
|
+ data.duration = videoItem.value.duration()
|
|
|
+
|
|
|
+ // 必须在当前元素
|
|
|
+
|
|
|
+ if (item.value.autoPlay && videoItem.value && props.isActive) {
|
|
|
+ videoItem.value?.play()
|
|
|
+ }
|
|
|
+ emit('loadedmetadata', videoItem.value)
|
|
|
+ })
|
|
|
+
|
|
|
+ // 视频播放时加载
|
|
|
+ videoItem.value.on('timeupdate', () => {
|
|
|
+ if (data.timer) return
|
|
|
+ data.currentTime = videoItem.value.currentTime()
|
|
|
+ })
|
|
|
+
|
|
|
+ // 视频播放结束
|
|
|
+ videoItem.value.on('ended', () => {
|
|
|
+ changePlayBtn('play')
|
|
|
+ emit('ended')
|
|
|
+ })
|
|
|
+
|
|
|
+ //
|
|
|
+ videoItem.value.on('pause', () => {
|
|
|
+ data.playState = 'pause'
|
|
|
+ changePlayBtn('pause')
|
|
|
+ emit('togglePlay', true)
|
|
|
+ })
|
|
|
+
|
|
|
videoItem.value.on('play', () => {
|
|
|
+ // console.log(play, 'playing')
|
|
|
+ changePlayBtn('play')
|
|
|
if (videoItem.value) {
|
|
|
videoItem.value.muted = false
|
|
|
videoItem.value.volume = 1
|
|
|
}
|
|
|
-
|
|
|
// console.log('开始播放', item.value)
|
|
|
if (!item.value.autoPlay && !item.value.isprepare && videoItem.value) {
|
|
|
// 加载完成后,取消静音播放
|
|
|
-
|
|
|
console.log(videoItem.value)
|
|
|
videoItem.value.pause()
|
|
|
}
|
|
|
- changePlayBtn('')
|
|
|
emit('togglePlay', videoItem.value?.paused)
|
|
|
})
|
|
|
- videoItem.value.on('pause', () => {
|
|
|
- changePlayBtn('play')
|
|
|
- emit('togglePlay', videoItem.value?.paused)
|
|
|
- })
|
|
|
- videoItem.value.on('ended', (e: Event) => {
|
|
|
- emit('ended')
|
|
|
- changePlayBtn('play')
|
|
|
- })
|
|
|
- videoItem.value.once('loadedmetadata', (e: Event) => {
|
|
|
- changePlayBtn('play')
|
|
|
- if (item.value.autoPlay && videoItem.value) {
|
|
|
- videoItem.value.play()
|
|
|
- }
|
|
|
- emit('loadedmetadata', videoItem.value)
|
|
|
- })
|
|
|
|
|
|
- nextTick(() => {
|
|
|
- onDefault()
|
|
|
+ // 视频播放异常
|
|
|
+ videoItem.value.on('error', (e: any) => {
|
|
|
+ emit('error')
|
|
|
+ console.log(e, 'error')
|
|
|
})
|
|
|
}
|
|
|
})
|
|
@@ -168,8 +197,55 @@ export default defineComponent({
|
|
|
style={{ width: '100%', height: '100%' }}
|
|
|
src={isEmtry.value ? '' : item.value.content}
|
|
|
ref={videoRef}
|
|
|
- playsinline="false"
|
|
|
+ id={videoID}
|
|
|
+ preload="auto"
|
|
|
+ playsinline
|
|
|
+ webkit-playsinline
|
|
|
></video>
|
|
|
+ <div class={styles.videoSection}></div>
|
|
|
+
|
|
|
+ <div
|
|
|
+ class={[styles.controls, data.showBar ? '' : styles.hide]}
|
|
|
+ onClick={(e: Event) => {
|
|
|
+ e.stopPropagation()
|
|
|
+ }}
|
|
|
+ // onTouchmove={(e: TouchEvent) => {
|
|
|
+ // emit('close')
|
|
|
+ // }}
|
|
|
+ >
|
|
|
+ <div class={styles.time}>
|
|
|
+ <div>{getSecondRPM(data.currentTime)}</div>
|
|
|
+ <div>{getSecondRPM(data.duration)}</div>
|
|
|
+ </div>
|
|
|
+ <div class={styles.slider}>
|
|
|
+ <Slider
|
|
|
+ step={0.01}
|
|
|
+ class={styles.timeProgress}
|
|
|
+ v-model={data.currentTime}
|
|
|
+ max={data.duration}
|
|
|
+ onUpdate:modelValue={(val) => {
|
|
|
+ handleChangeTime(val)
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div class={styles.actionSection}>
|
|
|
+ <div class={styles.actions} onClick={() => emit('close')}>
|
|
|
+ <div
|
|
|
+ class={styles.actionBtn}
|
|
|
+ onClick={(e: any) => {
|
|
|
+ e.stopPropagation()
|
|
|
+ onToggleAudio(data.playState === 'pause' ? 'play' : 'pause')
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <img src={data.playState === 'pause' ? iconplay : iconpause} />
|
|
|
+ </div>
|
|
|
+ <div class={styles.actionBtn} onClick={toggleLoop}>
|
|
|
+ <img src={data.loop ? iconLoopActive : iconLoop} />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class={styles.name}>{item.value.name}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
)
|
|
|
}
|