video-play.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. import { defineComponent, nextTick, onMounted, toRefs } from 'vue'
  2. import 'plyr/dist/plyr.css'
  3. import Plyr from 'plyr'
  4. import { ref } from 'vue'
  5. import styles from './video.module.less'
  6. import iconLoop from '../image/icon-loop.svg'
  7. import iconLoopActive from '../image/icon-loop-active.svg'
  8. import iconplay from '../image/icon-play.svg'
  9. import iconpause from '../image/icon-pause.svg'
  10. export default defineComponent({
  11. name: 'video-play',
  12. props: {
  13. item: {
  14. type: Object,
  15. default: () => {
  16. return {}
  17. }
  18. }
  19. },
  20. emits: ['loadedmetadata', 'togglePlay', 'ended', 'reset'],
  21. setup(props, { emit, expose }) {
  22. const { item } = toRefs(props)
  23. const videoRef = ref()
  24. const videoItem = ref<Plyr>()
  25. const controlID = 'v' + Date.now() + Math.floor(Math.random() * 100)
  26. const playBtnId = 'play' + Date.now() + Math.floor(Math.random() * 100)
  27. const loopBtnId = 'loop' + Date.now() + Math.floor(Math.random() * 100)
  28. const toggleHideControl = (isShow: false) => {
  29. videoItem.value?.toggleControls(isShow)
  30. }
  31. const togglePlay = (e: Event) => {
  32. e.stopPropagation()
  33. videoItem.value?.togglePlay()
  34. }
  35. const toggleLoop = (e: Event) => {
  36. const loopBtn = document.getElementById(loopBtnId)
  37. if (!loopBtn || !videoItem.value) return
  38. const isLoop = videoItem.value.loop
  39. if (isLoop) {
  40. loopBtn.classList.remove(styles.active)
  41. } else {
  42. loopBtn.classList.add(styles.active)
  43. }
  44. videoItem.value.loop = !videoItem.value.loop
  45. }
  46. const onDefault = () => {
  47. document.getElementById(controlID)?.addEventListener('click', (e: Event) => {
  48. e.stopPropagation()
  49. emit('reset')
  50. })
  51. document.getElementById(playBtnId)?.addEventListener('click', togglePlay)
  52. document.getElementById(loopBtnId)?.addEventListener('click', toggleLoop)
  53. }
  54. const changePlayBtn = (code: string) => {
  55. const playBtn = document.getElementById(playBtnId)
  56. if (!playBtn) return
  57. if (code == 'play') {
  58. playBtn.classList.remove(styles.btnPause)
  59. playBtn.classList.add(styles.btnPlay)
  60. } else {
  61. playBtn.classList.remove(styles.btnPlay)
  62. playBtn.classList.add(styles.btnPause)
  63. }
  64. }
  65. const controls = `
  66. <div id="${controlID}" class="plyr__controls bottomFixed ${styles.controls}">
  67. <div class="${styles.time}">
  68. <div class="plyr__time plyr__time--current" aria-label="Current time">00:00</div>
  69. <div class="plyr__time plyr__time--duration" aria-label="Duration">00:00</div>
  70. </div>
  71. <div class="${styles.slider}">
  72. <div class="plyr__progress">
  73. <input data-plyr="seek" type="range" min="0" max="100" step="0.01" value="0" aria-label="Seek">
  74. <progress class="plyr__progress__buffer" min="0" max="100" value="0">% buffered</progress>
  75. <span role="tooltip" class="plyr__tooltip">00:00</span>
  76. </div>
  77. </div>
  78. <div class="${styles.actions}">
  79. <div class="${styles.actionWrap}">
  80. <button id="${playBtnId}" class="${styles.actionBtn}">
  81. <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>
  82. <img class="${styles.playIcon}" src="${iconplay}" />
  83. <img class="${styles.playIcon}" src="${iconpause}" />
  84. </button>
  85. <button id="${loopBtnId}" class="${styles.actionBtn} ${styles.loopBtn}">
  86. <img class="loop" src="${iconLoop}" />
  87. <img class="loopActive" src="${iconLoopActive}" />
  88. </button>
  89. </div>
  90. <div>${item.value.name}</div>
  91. </div>
  92. </div>`
  93. onMounted(() => {
  94. videoItem.value = new Plyr(videoRef.value, {
  95. muted: true,
  96. autoplay: true,
  97. controls: controls,
  98. autopause: true, // 一次只允许
  99. ratio: '16:9', // 强制所有视频的纵横比
  100. hideControls: false, // 在 2 秒没有鼠标或焦点移动、控制元素模糊(制表符退出)、播放开始或进入全屏时自动隐藏视频控件。只要移动鼠标、聚焦控制元素或暂停播放,控件就会立即重新出现。
  101. clickToPlay: false, // 单击(或点击)视频容器将切换播放/暂停
  102. fullscreen: { enabled: false, fallback: false, iosNative: false } // 不适用全屏
  103. })
  104. if (videoItem.value) {
  105. videoItem.value.on('play', () => {
  106. // console.log('开始播放', item.value)
  107. if (!item.value.autoPlay && !item.value.isprepare && videoItem.value) {
  108. // 加载完成后,取消静音播放
  109. videoItem.value.muted = false
  110. videoItem.value.pause()
  111. }
  112. changePlayBtn('')
  113. emit('togglePlay', videoItem.value?.paused)
  114. })
  115. videoItem.value.on('pause', () => {
  116. changePlayBtn('play')
  117. emit('togglePlay', videoItem.value?.paused)
  118. })
  119. videoItem.value.on('ended', (e: Event) => {
  120. emit('ended')
  121. changePlayBtn('play')
  122. })
  123. videoItem.value.once('loadedmetadata', (e: Event) => {
  124. changePlayBtn('play')
  125. if (item.value.autoPlay && videoItem.value) {
  126. videoItem.value.play()
  127. }
  128. emit('loadedmetadata', videoItem.value)
  129. })
  130. nextTick(() => {
  131. onDefault()
  132. })
  133. }
  134. })
  135. expose({
  136. changePlayBtn,
  137. toggleHideControl
  138. })
  139. return () => (
  140. <div class={styles.videoWrap}>
  141. <video
  142. style={{ width: '100%', height: '100%' }}
  143. src={item.value.content}
  144. ref={videoRef}
  145. playsinline="false"
  146. ></video>
  147. </div>
  148. )
  149. }
  150. })