index.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. import { defineComponent, nextTick, onMounted, reactive, toRefs, watch } from "vue"
  2. import "plyr/dist/plyr.css"
  3. import Plyr from "plyr"
  4. import styles from "./index.module.scss"
  5. import icons from "../../image/icons.json"
  6. const { iconVideoBg } = icons
  7. // eslint-disable-next-line @typescript-eslint/no-var-requires
  8. const iconLoopActive = require("../../image/iconLoopActive.png")
  9. // eslint-disable-next-line @typescript-eslint/no-var-requires
  10. const iconLoop = require("../../image/iconLoop.png")
  11. // eslint-disable-next-line @typescript-eslint/no-var-requires
  12. const iconPause = require("../../image/iconPause.png")
  13. // eslint-disable-next-line @typescript-eslint/no-var-requires
  14. const iconPlay = require("../../image/iconPlay.png")
  15. // eslint-disable-next-line @typescript-eslint/no-var-requires
  16. const iconSpeed = require("../../image/iconSpeed.png")
  17. import { ElSlider } from "element-plus"
  18. export default defineComponent({
  19. name: "video-play",
  20. props: {
  21. item: {
  22. type: Object,
  23. default: () => {
  24. return {}
  25. }
  26. },
  27. activeModel: {
  28. type: Boolean,
  29. default: true
  30. }
  31. },
  32. emits: ["play", "pause", "ended", "close"],
  33. setup(props, { emit, expose }) {
  34. const { item } = toRefs(props)
  35. const data = reactive({
  36. videoContianerRef: null as unknown as HTMLAudioElement,
  37. videoState: "pause" as "init" | "play" | "pause",
  38. animationState: "start" as "start" | "end",
  39. videoItem: null as unknown as Plyr,
  40. speedControl: false,
  41. speedStyle: {
  42. left: "1px"
  43. },
  44. defaultSpeed: 1 // 默认速度
  45. })
  46. const controlID = "v" + Date.now() + Math.floor(Math.random() * 100)
  47. const playBtnId = "play" + Date.now() + Math.floor(Math.random() * 100)
  48. const loopBtnId = "loop" + Date.now() + Math.floor(Math.random() * 100)
  49. const speedBtnId = "speed" + Date.now() + Math.floor(Math.random() * 100)
  50. const togglePlay = (e: Event) => {
  51. e.stopPropagation()
  52. data.speedControl = false
  53. if (!data.videoContianerRef.paused) {
  54. data.videoItem?.pause()
  55. } else {
  56. data.videoContianerRef?.play()
  57. }
  58. }
  59. const toggleLoop = () => {
  60. data.speedControl = false
  61. const loopBtn = document.getElementById(loopBtnId)
  62. if (!loopBtn || !data.videoItem) return
  63. const isLoop = data.videoItem.loop
  64. if (isLoop) {
  65. loopBtn.classList.remove(styles.active)
  66. } else {
  67. loopBtn.classList.add(styles.active)
  68. }
  69. data.videoItem.loop = !data.videoItem.loop
  70. }
  71. const onDefault = () => {
  72. document.getElementById(controlID)?.addEventListener("click", (e: Event) => {
  73. e.stopPropagation()
  74. data.speedControl = false
  75. if (data.videoContianerRef.paused) return
  76. emit("close")
  77. })
  78. document.getElementById(controlID)?.addEventListener("touchmove", () => {
  79. data.speedControl = false
  80. if (data.videoContianerRef.paused) return
  81. emit("close")
  82. })
  83. document.getElementById(playBtnId)?.addEventListener("click", togglePlay)
  84. document.getElementById(loopBtnId)?.addEventListener("click", toggleLoop)
  85. document.getElementById(speedBtnId)?.addEventListener("click", e => {
  86. e.stopPropagation()
  87. data.speedControl = !data.speedControl
  88. })
  89. setName()
  90. }
  91. const setName = () => {
  92. const nameEl = document.getElementById("videoItemName")
  93. if (nameEl) {
  94. nameEl.innerHTML = item.value.name || ""
  95. }
  96. }
  97. const changePlayBtn = (code: string) => {
  98. const playBtn = document.getElementById(playBtnId)
  99. if (!playBtn) return
  100. if (code == "play") {
  101. playBtn.classList.remove(styles.btnPause)
  102. playBtn.classList.add(styles.btnPlay)
  103. } else {
  104. playBtn.classList.remove(styles.btnPlay)
  105. playBtn.classList.add(styles.btnPause)
  106. }
  107. }
  108. const controls = `
  109. <div id="${controlID}" class="plyr__controls bottomFixed ${styles.controls}">
  110. <div class="${styles.time}">
  111. <div class="plyr__time plyr__time--current" aria-label="Current time">00:00</div>
  112. <div class="plyr__time plyr__time--duration" aria-label="Duration">00:00</div>
  113. </div>
  114. <div class="${styles.slider}">
  115. <div class="plyr__progress">
  116. <input data-plyr="seek" type="range" min="0" max="100" step="0.01" value="0" aria-label="Seek">
  117. <progress class="plyr__progress__buffer" min="0" max="100" value="0">% buffered</progress>
  118. <span role="tooltip" class="plyr__tooltip">00:00</span>
  119. </div>
  120. </div>
  121. <div class="${styles.actions}">
  122. <div class="${styles.actionWrap}">
  123. <div id="${playBtnId}" class="${styles.actionBtn}">
  124. <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>
  125. <img class="${styles.playIcon}" src="${iconPlay}" />
  126. <img class="${styles.playIcon}" src="${iconPause}" />
  127. </div>
  128. <div id="${loopBtnId}" class="${styles.actionBtn} ${styles.loopBtn}">
  129. <img class="loop" style="width:54px;height:44px;" src="${iconLoop}" />
  130. <img class="loopActive" style="width:58px;height:44px;" src="${iconLoopActive}" />
  131. </div>
  132. <div style="position: relative">
  133. <div id="${speedBtnId}" class="${styles.actionBtn} ${styles.speedBtn}">
  134. <img class="loop" src="${iconSpeed}" />
  135. </div>
  136. </div>
  137. </div>
  138. <div id="videoItemName"></div>
  139. </div>
  140. </div>`
  141. onMounted(() => {
  142. data.videoItem = new Plyr(data.videoContianerRef, {
  143. autoplay: true,
  144. controls: controls,
  145. ratio: "16:9", // 强制所有视频的纵横比
  146. hideControls: false, // 在 2 秒没有鼠标或焦点移动、控制元素模糊(制表符退出)、播放开始或进入全屏时自动隐藏视频控件。只要移动鼠标、聚焦控制元素或暂停播放,控件就会立即重新出现。
  147. clickToPlay: false, // 单击(或点击)视频容器将切换播放/暂停
  148. fullscreen: { enabled: false, fallback: false, iosNative: false } // 不适用全屏
  149. })
  150. nextTick(() => {
  151. onDefault()
  152. })
  153. })
  154. const toggleHideControl = (isShow: boolean) => {
  155. data.videoItem?.toggleControls(isShow)
  156. if (!isShow) {
  157. data.speedControl = isShow
  158. }
  159. }
  160. watch(
  161. () => props.activeModel,
  162. () => {
  163. toggleHideControl(props.activeModel)
  164. }
  165. )
  166. watch(
  167. () => props.item,
  168. () => {
  169. setName()
  170. // 设置视屏播放器的默认速度
  171. if (data.videoItem) data.videoItem.speed = data.defaultSpeed || 1
  172. // 切换的时候隐藏
  173. data.speedControl = false
  174. }
  175. )
  176. let videoTimer = null as any
  177. const handlePlayVideo = () => {
  178. clearTimeout(videoTimer)
  179. nextTick(() => {
  180. data.videoContianerRef.play().catch(err => {
  181. console.log("🚀 ~ err:", err)
  182. videoTimer = setTimeout(() => {
  183. if (err?.message?.includes("play()")) {
  184. emit("play")
  185. }
  186. handlePlayVideo()
  187. }, 1000)
  188. })
  189. })
  190. }
  191. let videoErrorTimer = null as any
  192. let videoErrorCount = 0
  193. const handleErrorVideo = () => {
  194. if (videoErrorCount > 5) {
  195. return
  196. }
  197. clearTimeout(videoErrorTimer)
  198. nextTick(() => {
  199. videoErrorTimer = setTimeout(() => {
  200. data.videoContianerRef.src = props.item?.content
  201. emit("play")
  202. data.videoContianerRef.load()
  203. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  204. handleErrorVideo()
  205. }, 1000)
  206. })
  207. videoErrorCount++
  208. }
  209. const getVideoRef = () => {
  210. return data.videoContianerRef
  211. }
  212. const getVideoItem = () => {
  213. return data.videoItem
  214. }
  215. expose({
  216. getVideoRef,
  217. getVideoItem
  218. })
  219. return () => (
  220. <div class={styles.videoWrap}>
  221. <video
  222. ref={el => (data.videoContianerRef = el as unknown as HTMLAudioElement)}
  223. class={styles.itemDiv}
  224. src={props.item?.content}
  225. poster={iconVideoBg}
  226. webkit-playsinline
  227. playsinline
  228. x5-video-player-type="h5"
  229. onLoadedmetadata={() => {
  230. data.videoState = "pause"
  231. changePlayBtn("play")
  232. nextTick(() => {
  233. data.videoContianerRef.currentTime = 0
  234. nextTick(handlePlayVideo)
  235. })
  236. }}
  237. onPlay={() => {
  238. videoErrorCount = 0
  239. // console.log('开始播放')
  240. data.videoState = "play"
  241. changePlayBtn("pause")
  242. emit("close")
  243. emit("play")
  244. clearTimeout(videoErrorTimer)
  245. }}
  246. onPause={() => {
  247. // console.log('暂停播放')
  248. data.videoState = "pause"
  249. changePlayBtn("play")
  250. emit("pause")
  251. }}
  252. onEnded={() => {
  253. // console.log('播放结束')
  254. data.videoState = "pause"
  255. changePlayBtn("play")
  256. emit("ended")
  257. }}
  258. onError={handleErrorVideo}
  259. ></video>
  260. <div
  261. style={{
  262. display: data.speedControl ? "block" : "none"
  263. }}
  264. >
  265. <div
  266. class={styles.sliderPopup}
  267. onClick={(e: Event) => {
  268. e.stopPropagation()
  269. }}
  270. >
  271. <i
  272. class={styles.iconAdd}
  273. onClick={() => {
  274. if (data.defaultSpeed >= 1.5) {
  275. return
  276. }
  277. if (data.videoItem) {
  278. data.defaultSpeed = (data.defaultSpeed * 10 + 1) / 10
  279. data.videoItem.speed = data.defaultSpeed
  280. }
  281. }}
  282. ></i>
  283. <ElSlider
  284. class={styles.el_slider}
  285. style={{ padding: "12px 0" }}
  286. min={0.5}
  287. max={1.5}
  288. step={0.1}
  289. v-model={data.defaultSpeed}
  290. vertical
  291. height={"82px"}
  292. onChange={() => {
  293. if (data.videoItem) {
  294. data.videoItem.speed = data.defaultSpeed
  295. }
  296. }}
  297. >
  298. {{
  299. button: () => (
  300. <div class={styles.sliderPoint}>
  301. {data.defaultSpeed}
  302. <span>x</span>
  303. </div>
  304. )
  305. }}
  306. </ElSlider>
  307. <i
  308. class={[styles.iconCut]}
  309. onClick={() => {
  310. if (data.defaultSpeed <= 0.5) {
  311. return
  312. }
  313. if (data.videoItem) {
  314. data.defaultSpeed = (data.defaultSpeed * 10 - 1) / 10
  315. data.videoItem.speed = data.defaultSpeed
  316. }
  317. }}
  318. ></i>
  319. </div>
  320. </div>
  321. </div>
  322. )
  323. }
  324. })