video-play.tsx 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. import { defineComponent, nextTick, onMounted, reactive, toRefs, watch } 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. import TCPlayer from 'tcplayer.js'
  11. import 'tcplayer.js/dist/tcplayer.min.css'
  12. import { Slider } from 'vant'
  13. // 秒转分
  14. export const getSecondRPM = (second: number, type?: string) => {
  15. if (isNaN(second)) return '00:00'
  16. const mm = Math.floor(second / 60)
  17. .toString()
  18. .padStart(2, '0')
  19. const dd = Math.floor(second % 60)
  20. .toString()
  21. .padStart(2, '0')
  22. if (type === 'cn') {
  23. return mm + '分' + dd + '秒'
  24. } else {
  25. return mm + ':' + dd
  26. }
  27. }
  28. export default defineComponent({
  29. name: 'video-play',
  30. props: {
  31. item: {
  32. type: Object,
  33. default: () => {
  34. return {}
  35. }
  36. },
  37. isEmtry: {
  38. type: Boolean,
  39. default: false
  40. },
  41. isActive: {
  42. type: Boolean,
  43. default: false
  44. },
  45. activeModel: {
  46. type: Boolean,
  47. default: true
  48. }
  49. },
  50. emits: [
  51. 'loadedmetadata',
  52. 'togglePlay',
  53. 'ended',
  54. 'reset',
  55. 'error',
  56. 'close',
  57. 'play',
  58. 'pause',
  59. 'seeked',
  60. 'seeking',
  61. 'waiting',
  62. 'timeupdate'
  63. ],
  64. setup(props, { emit, expose }) {
  65. const { item, isEmtry } = toRefs(props)
  66. const data = reactive({
  67. timer: null as any,
  68. currentTime: 0,
  69. duration: 0.1,
  70. loop: false,
  71. playState: 'pause' as 'play' | 'pause',
  72. vudio: null as any,
  73. showBar: true
  74. })
  75. const videoRef = ref()
  76. const videoItem = ref()
  77. const videoID = 'video' + Date.now() + Math.floor(Math.random() * 100)
  78. const toggleHideControl = (isShow: false) => {
  79. data.showBar = isShow
  80. }
  81. // const togglePlay = (e: Event) => {
  82. // e.stopPropagation()
  83. // }
  84. let playTimer = null as any
  85. // 切换音频播放
  86. const onToggleAudio = (state: 'play' | 'pause') => {
  87. // console.log(state, 'state')
  88. clearTimeout(playTimer)
  89. if (state === 'play') {
  90. playTimer = setTimeout(() => {
  91. videoItem.value?.play()
  92. data.playState = 'play'
  93. }, 100)
  94. } else {
  95. videoItem.value?.pause()
  96. data.playState = 'pause'
  97. }
  98. emit('togglePlay', data.playState)
  99. }
  100. const toggleLoop = () => {
  101. if (!videoItem.value) return
  102. if (data.loop) {
  103. videoItem.value.loop(false)
  104. } else {
  105. videoItem.value.loop(true)
  106. }
  107. data.loop = !data.loop
  108. }
  109. const changePlayBtn = (code: string) => {
  110. if (code == 'play') {
  111. data.playState = 'play'
  112. } else {
  113. data.playState = 'pause'
  114. }
  115. }
  116. /** 改变播放时间 */
  117. const handleChangeTime = (val: number) => {
  118. data.currentTime = val
  119. clearTimeout(data.timer)
  120. data.timer = setTimeout(() => {
  121. videoItem.value.currentTime(val)
  122. data.timer = null
  123. }, 300)
  124. }
  125. const __initVideo = () => {
  126. if (videoItem.value && props.item.id) {
  127. videoItem.value.poster(props.item.coverImg) // 封面
  128. videoItem.value.src(item.value.content) // url 播放地址
  129. // 初步加载时
  130. videoItem.value.on('loadedmetadata', (e: any) => {
  131. console.log(' Loading metadata')
  132. // 获取时长
  133. data.duration = videoItem.value.duration()
  134. // 必须在当前元素
  135. if (item.value.autoPlay && videoItem.value && props.isActive) {
  136. // videoItem.value?.play()
  137. nextTick(() => {
  138. videoItem.value.currentTime(0)
  139. nextTick(handlePlayVideo)
  140. })
  141. }
  142. emit('loadedmetadata', videoItem.value)
  143. })
  144. // videoItem.value.on('timeupdate', () => {
  145. // if (!props.isActive) {
  146. // console.log('不是激活的视频,如果在播放,就暂停')
  147. // videoRef.value.pause()
  148. // }
  149. // })
  150. // 视频播放时加载
  151. videoItem.value.on('timeupdate', () => {
  152. if (data.timer) return
  153. data.currentTime = videoItem.value.currentTime()
  154. emit('timeupdate')
  155. })
  156. // 视频播放结束
  157. videoItem.value.on('ended', () => {
  158. changePlayBtn('play')
  159. emit('ended')
  160. })
  161. //
  162. videoItem.value.on('pause', () => {
  163. data.playState = 'pause'
  164. changePlayBtn('pause')
  165. emit('togglePlay', true)
  166. emit('pause')
  167. })
  168. videoItem.value.on('seeked', () => {
  169. emit('seeked')
  170. })
  171. videoItem.value.on('seeking', () => {
  172. emit('seeking')
  173. })
  174. videoItem.value.on('waiting', () => {
  175. emit('waiting')
  176. })
  177. videoItem.value.on('play', () => {
  178. // console.log(play, 'playing')
  179. changePlayBtn('play')
  180. if (videoItem.value) {
  181. videoItem.value.muted = false
  182. videoItem.value.volume(1)
  183. }
  184. if (!item.value.autoPlay && !item.value.isprepare && videoItem.value) {
  185. // 加载完成后,取消静音播放
  186. // console.log(videoItem.value)
  187. videoItem.value.pause()
  188. }
  189. emit('togglePlay', videoItem.value?.paused)
  190. emit('play')
  191. })
  192. // 视频播放异常
  193. videoItem.value.on('error', (e: any) => {
  194. handleErrorVideo()
  195. emit('error')
  196. console.log(e, 'error')
  197. })
  198. }
  199. }
  200. let videoTimer = null as any
  201. let videoTimerErrorCount = 0
  202. const handlePlayVideo = () => {
  203. if (videoTimerErrorCount > 5) {
  204. return
  205. }
  206. clearTimeout(videoTimer)
  207. nextTick(() => {
  208. videoItem.value?.play().catch((err) => {
  209. // console.log('🚀 ~ err:', err)
  210. videoTimer = setTimeout(() => {
  211. if (err?.message?.includes('play()')) {
  212. emit('play')
  213. }
  214. handlePlayVideo()
  215. }, 1000)
  216. })
  217. })
  218. videoTimerErrorCount++
  219. }
  220. let videoErrorTimer = null as any
  221. let videoErrorCount = 0
  222. const handleErrorVideo = () => {
  223. if (videoErrorCount > 5) {
  224. return
  225. }
  226. clearTimeout(videoErrorTimer)
  227. nextTick(() => {
  228. videoErrorTimer = setTimeout(() => {
  229. videoItem.value.src = props.item?.content
  230. emit('play')
  231. videoItem.value.load()
  232. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  233. handleErrorVideo()
  234. }, 1000)
  235. })
  236. videoErrorCount++
  237. }
  238. onMounted(() => {
  239. videoItem.value = TCPlayer(videoID, {
  240. appID: '',
  241. controls: false
  242. // autoplay: true
  243. }) // player-container-id 为播放器容器 ID,必须与 html 中一致
  244. __initVideo()
  245. })
  246. watch(
  247. () => props.item,
  248. () => {
  249. __initVideo()
  250. }
  251. )
  252. const getVideoRef = () => {
  253. return videoRef.value
  254. }
  255. const getPlyrRef = () => {
  256. return videoItem.value
  257. }
  258. expose({
  259. changePlayBtn,
  260. toggleHideControl,
  261. getVideoRef,
  262. getPlyrRef
  263. })
  264. watch(
  265. () => props.isActive,
  266. (val) => {
  267. if (!val) {
  268. videoItem.value?.pause()
  269. }
  270. }
  271. )
  272. return () => (
  273. <div class={styles.videoWrap}>
  274. <video
  275. style={{ width: '100%', height: '100%' }}
  276. src={item.value.content}
  277. ref={videoRef}
  278. id={videoID}
  279. preload="auto"
  280. playsinline
  281. webkit-playsinline
  282. ></video>
  283. <div class={styles.videoSection}></div>
  284. <div
  285. class={[styles.controls, data.showBar ? '' : styles.hide]}
  286. onClick={(e: Event) => {
  287. e.stopPropagation()
  288. }}
  289. // onTouchmove={(e: TouchEvent) => {
  290. // emit('close')
  291. // }}
  292. >
  293. <div class={styles.time}>
  294. <div>{getSecondRPM(data.currentTime)}</div>
  295. <div>{getSecondRPM(data.duration)}</div>
  296. </div>
  297. <div class={styles.slider}>
  298. <Slider
  299. step={0.01}
  300. class={styles.timeProgress}
  301. v-model={data.currentTime}
  302. max={data.duration}
  303. onUpdate:modelValue={(val) => {
  304. handleChangeTime(val)
  305. }}
  306. />
  307. </div>
  308. <div class={styles.actionSection}>
  309. <div class={styles.actions} onClick={() => emit('close')}>
  310. <div
  311. class={styles.actionBtn}
  312. onClick={(e: any) => {
  313. e.stopPropagation()
  314. onToggleAudio(data.playState === 'pause' ? 'play' : 'pause')
  315. }}
  316. >
  317. <img src={data.playState === 'pause' ? iconplay : iconpause} />
  318. </div>
  319. <div class={styles.actionBtn} onClick={toggleLoop}>
  320. <img src={data.loop ? iconLoopActive : iconLoop} />
  321. </div>
  322. </div>
  323. <div class={styles.name}>{item.value.name}</div>
  324. </div>
  325. </div>
  326. </div>
  327. )
  328. }
  329. })