videoPlay.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. <!--
  2. * @FileDescription: 播放器
  3. * @Author: 黄琪勇
  4. * @Date:2024-04-03 18:30:09
  5. -->
  6. <template>
  7. <div
  8. @keydown="handleVideoKeydown"
  9. @mousemove="handleVideoMousemove"
  10. @click="handleVideoClick"
  11. class="videoPlay"
  12. :class="{ isHideController: !isShowController }"
  13. tabindex="-1"
  14. >
  15. <video class="videoPlayBox" :id="videoId" preload="auto" playsinline webkit-playsinline></video>
  16. <div class="videoController" @click.stop @touchstart.stop>
  17. <div class="sliderSection">
  18. <div class="timeController">{{ `${formatTime(timeController.currentTime)} / ${formatTime(timeController.duration)}` }}</div>
  19. <n-slider
  20. class="sliderController"
  21. :keyboard="false"
  22. :value="timeController.currentTimeSilder"
  23. :tooltip="isShowController"
  24. :step="0.01"
  25. @update:value="handleSilderChange"
  26. :on-dragend="handleTimeChange"
  27. :max="timeController.duration"
  28. :format-tooltip="(value:number) => {
  29. return formatTime(value)
  30. }"
  31. />
  32. </div>
  33. <div class="playController">
  34. <div class="leftPlayController">
  35. <img @click="handlePlay" :src="require(`./img/${playController.type === 'play' ? 'iconPause' : 'iconPlay'}.png`)" />
  36. <img @click="handleLoop" class="loopImg" :src="require(`./img/${playController.loop ? 'iconLoopActive' : 'iconLoop'}.png`)" />
  37. <img ref="btnSpendDom" src="./img//iconSpeed.png" />
  38. <el-popover
  39. @show="handlePopoverTimeHide"
  40. ref="popoverSpendDom"
  41. :virtual-ref="btnSpendDom"
  42. trigger="click"
  43. placement="top"
  44. :teleported="false"
  45. popper-class="palySpeedPopover"
  46. virtual-triggering
  47. >
  48. <div class="sliderSpeedCon">
  49. <img @click="handlePalySpeed(playController.speedStep)" src="./img/jia.png" />
  50. <n-slider
  51. class="sliderSpeed"
  52. :tooltip="false"
  53. :keyboard="false"
  54. :value="playController.palySpeed"
  55. @update:value="handlePalySpeedChange"
  56. vertical
  57. :step="playController.speedStep"
  58. :max="playController.maxSpeed"
  59. :min="playController.minSpeed"
  60. >
  61. <template #thumb>
  62. <div class="thumb">{{ playController.palySpeed.toFixed(1) + "X" }}</div>
  63. </template>
  64. </n-slider>
  65. <img @click="handlePalySpeed(-playController.speedStep)" src="./img/jian.png" />
  66. </div>
  67. </el-popover>
  68. </div>
  69. <div class="rightPlayController">
  70. <div class="videoName">
  71. <!-- <div>{{ videoName || "" }}</div> -->
  72. </div>
  73. </div>
  74. </div>
  75. </div>
  76. </div>
  77. </template>
  78. <script setup lang="ts">
  79. import TCPlayer from "tcplayer.js"
  80. import "tcplayer.js/dist/tcplayer.min.css"
  81. import { onMounted, onUnmounted, ref, reactive, watch, toRef, watchEffect } from "vue"
  82. import { UUID } from "@/libs/tools"
  83. import { formatTime } from "./tools"
  84. import { NSlider } from "naive-ui"
  85. import { ElMessage } from "element-plus"
  86. const props = defineProps<{
  87. disableEvents?: boolean
  88. isShowController?: boolean
  89. autoPlay?: boolean
  90. }>()
  91. const emits = defineEmits<{
  92. (e: "ready"): void //播放器初始化完成
  93. (e: "ended"): void //播放结束
  94. (e: "playbackRate"): void //播放速度改动时候
  95. }>()
  96. const videoId = "video" + UUID()
  97. let playerVm: Record<string, any>
  98. let isReady = false // 是否初始化播放器播放器
  99. const videoName = ref("")
  100. /* 时间控制器 */
  101. const timeController = reactive({
  102. currentTime: 0, // 当前时间
  103. duration: 0, // 总时长
  104. currentTimeSilder: 0,
  105. isDrag: false
  106. })
  107. /* 播放控制器 */
  108. const btnSpendDom = ref()
  109. const popoverSpendDom = ref()
  110. let _popoverSpendTime: any
  111. // 定时隐藏
  112. function handlePopoverTimeHide() {
  113. _popoverSpendTime && clearTimeout(_popoverSpendTime)
  114. _popoverSpendTime = setTimeout(() => {
  115. popoverSpendDom.value?.hide()
  116. }, 5000)
  117. }
  118. const playController = reactive<{
  119. type: "play" | "pause"
  120. loop: boolean
  121. minSpeed: number
  122. maxSpeed: number
  123. speedStep: number
  124. palySpeed: number
  125. }>({
  126. type: "pause", //play|pause
  127. loop: false,
  128. minSpeed: 0.5,
  129. maxSpeed: 1.5,
  130. speedStep: 0.1,
  131. palySpeed: 1
  132. })
  133. watch(
  134. () => playController.palySpeed,
  135. () => {
  136. // 值变化 重新计算隐藏时间
  137. handlePopoverTimeHide()
  138. }
  139. )
  140. /* 是否显示控制器 */
  141. const isShowController = ref(true)
  142. watchEffect(() => {
  143. isShowController.value = props.isShowController
  144. })
  145. let _showTimer: any
  146. onMounted(() => {
  147. initVideo()
  148. })
  149. onUnmounted(() => {
  150. playerVm?.dispose()
  151. })
  152. /**
  153. * 初始化播放器
  154. */
  155. function initVideo() {
  156. playerVm = TCPlayer(videoId, {
  157. controls: false,
  158. //autoplay: true, // 自动播放前1秒暂停不了,改为loadedmetadata调用播放 实现自动播放
  159. loop: false
  160. })
  161. // 初始化完成
  162. playerVm.ready(() => {
  163. console.log("播放器初始化完成")
  164. isReady = true
  165. emits("ready")
  166. })
  167. // 开始加载数据时
  168. playerVm.on("loadstart", () => {
  169. // 重新设置播放倍速 因为切换视频播放倍速会重置
  170. playerVm.playbackRate(playController.palySpeed)
  171. })
  172. playerVm.on("loadedmetadata", () => {
  173. console.log("loadedmetadata")
  174. if (props.autoPlay) {
  175. playerVm.play()
  176. }
  177. })
  178. //总时长变化时候
  179. playerVm.on("durationchange", () => {
  180. timeController.duration = playerVm.duration()
  181. })
  182. //当前播放时间变化
  183. playerVm.on("timeupdate", () => {
  184. timeController.currentTime = playerVm.currentTime()
  185. if (!timeController.isDrag) {
  186. timeController.currentTimeSilder = timeController.currentTime
  187. }
  188. })
  189. playerVm.on("play", () => {
  190. playController.type = "play"
  191. })
  192. playerVm.on("pause", () => {
  193. playController.type = "pause"
  194. })
  195. // 播放结束
  196. playerVm.on("ended", () => {
  197. emits("ended")
  198. })
  199. }
  200. /**
  201. * 播放 需要在ready之后调用
  202. */
  203. // 接口请求可能在播放之前 所以这里等待播放器初始化
  204. let _time: any
  205. function playVideo({ src, name }: { src: string; name: string }) {
  206. videoName.value = name
  207. _time && clearInterval(_time)
  208. if (isReady) {
  209. handlePlayVideo(src)
  210. } else {
  211. _time = setInterval(() => {
  212. if (isReady) {
  213. clearInterval(_time)
  214. handlePlayVideo(src)
  215. }
  216. }, 60)
  217. }
  218. }
  219. function handlePlayVideo(src: string) {
  220. playerVm?.src(src)
  221. showController()
  222. }
  223. /* 时间控制器 */
  224. function handleTimeChange(value?: number) {
  225. playerVm.currentTime(value || timeController.currentTimeSilder)
  226. timeController.isDrag = false
  227. }
  228. function handleSilderChange(value: number) {
  229. timeController.isDrag = true
  230. timeController.currentTimeSilder = value
  231. }
  232. // 快进或者快退
  233. function speedCurrentTime(type: "fast" | "slow") {
  234. handleTimeChange(timeController.currentTime + (type === "fast" ? 5 : -5))
  235. showController()
  236. }
  237. /* 播放控制器 */
  238. function handlePlay() {
  239. playController.type === "pause" ? playerVm.play() : playerVm.pause()
  240. showController()
  241. }
  242. // 暂停
  243. function pauseVideo() {
  244. playerVm.pause()
  245. showController()
  246. }
  247. // 循环播放
  248. let message: any = null
  249. function handleLoop() {
  250. playController.loop ? playerVm.loop(false) : playerVm.loop(true)
  251. playController.loop = playerVm.loop()
  252. if (playController.loop) {
  253. if (message) {
  254. message.close()
  255. }
  256. message = ElMessage({
  257. message: "已打开循环播放",
  258. icon: "",
  259. customClass: "defaultMessage"
  260. })
  261. } else {
  262. if (message) {
  263. message.close()
  264. }
  265. message = ElMessage({
  266. message: "已关闭循环播放",
  267. icon: "",
  268. customClass: "defaultMessage"
  269. })
  270. }
  271. }
  272. // 播放速度
  273. function handlePalySpeedChange(value: number) {
  274. playController.palySpeed = value
  275. playerVm.playbackRate(value)
  276. emits("playbackRate")
  277. }
  278. function handlePalySpeed(value: number) {
  279. const palySpeed = parseFloat((playController.palySpeed + value).toFixed(1))
  280. if (palySpeed > playController.maxSpeed || palySpeed < playController.minSpeed) {
  281. return
  282. }
  283. handlePalySpeedChange(palySpeed)
  284. }
  285. function handleVideoClick() {
  286. if (props.disableEvents) return
  287. handlePlay()
  288. }
  289. function handleVideoKeydown(e: KeyboardEvent) {
  290. if (props.disableEvents) return
  291. const key = e.key
  292. if (key === " ") {
  293. handlePlay()
  294. } else if (key === "ArrowLeft") {
  295. speedCurrentTime("slow")
  296. } else if (key === "ArrowRight") {
  297. speedCurrentTime("fast")
  298. }
  299. }
  300. function handleVideoMousemove() {
  301. if (props.disableEvents) return
  302. showController()
  303. }
  304. /* 是否显示控制器 */
  305. function showController() {
  306. if (props.disableEvents) return
  307. isShowController.value = true
  308. _showTimer && clearTimeout(_showTimer)
  309. _showTimer = setTimeout(tryHideController, 3000)
  310. }
  311. function tryHideController() {
  312. if (playController.type === "play") {
  313. isShowController.value = false
  314. }
  315. }
  316. defineExpose({
  317. playVideo,
  318. pauseVideo,
  319. handlePlay,
  320. speedCurrentTime,
  321. playType: toRef(playController, "type")
  322. })
  323. </script>
  324. <style lang="scss" scoped>
  325. .videoPlay {
  326. width: 100%;
  327. height: 100%;
  328. position: relative;
  329. overflow: hidden;
  330. :deep(.videoPlayBox) {
  331. &.tcp-skin .tcp-right-click-popup-menu {
  332. display: none;
  333. }
  334. }
  335. .videoPlayBox {
  336. width: 100%;
  337. height: 100%;
  338. }
  339. &.isHideController {
  340. cursor: none;
  341. .videoController {
  342. opacity: 0;
  343. transform: translateY(100%);
  344. }
  345. }
  346. .videoController {
  347. position: absolute;
  348. width: 100%;
  349. left: 0;
  350. bottom: 0;
  351. padding: 45px 32px 22px;
  352. background: linear-gradient(0deg, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0) 100%);
  353. color: #fff;
  354. transition: all 0.5s;
  355. // &:hover { //取消鼠标移入不隐藏
  356. // cursor: initial;
  357. // opacity: initial !important;
  358. // transform: initial !important;
  359. // }
  360. .sliderSection {
  361. display: flex;
  362. align-items: center;
  363. & > :deep(.sliderController.n-slider) {
  364. --n-rail-color: rgba(255, 255, 255, 0.5) !important;
  365. --n-fill-color: #ff8057 !important;
  366. --n-fill-color-hover: #ff8057 !important;
  367. }
  368. }
  369. .timeController {
  370. font-weight: 500;
  371. font-size: 20px;
  372. color: #ffffff;
  373. line-height: 30px;
  374. flex-shrink: 0;
  375. margin-right: 16px;
  376. }
  377. .playController {
  378. display: flex;
  379. justify-content: space-between;
  380. padding-top: 4px;
  381. .leftPlayController {
  382. margin-left: -16px;
  383. display: flex;
  384. & > img {
  385. cursor: pointer;
  386. width: 48px;
  387. height: 48px;
  388. margin-right: 30px;
  389. padding: 6px;
  390. box-sizing: content-box;
  391. &:hover {
  392. background-color: rgba(255, 255, 255, 0.2);
  393. border-radius: 6px;
  394. }
  395. }
  396. .loopImg {
  397. width: 56px;
  398. }
  399. & > :deep(.palySpeedPopover.el-popover.el-popper) {
  400. min-width: initial;
  401. width: 59px !important;
  402. height: 264px;
  403. background: url("./img/bg.png") no-repeat;
  404. background-size: 100% 100%;
  405. box-shadow: none;
  406. border: none;
  407. padding: 12px 0 20px;
  408. .el-popper__arrow {
  409. display: none;
  410. }
  411. .sliderSpeedCon {
  412. width: 100%;
  413. height: 100%;
  414. display: flex;
  415. justify-content: center;
  416. align-items: center;
  417. flex-direction: column;
  418. & > img {
  419. cursor: pointer;
  420. flex-shrink: 0;
  421. width: 30px;
  422. height: 31px;
  423. }
  424. .sliderSpeed.n-slider {
  425. flex-grow: 1;
  426. padding: 6px 0;
  427. --n-rail-width-vertical: 5px !important;
  428. --n-rail-color: rgba(255, 255, 255, 0.5) !important;
  429. --n-fill-color: #ff8057 !important;
  430. --n-fill-color-hover: #ff8057 !important;
  431. .thumb {
  432. height: 22px;
  433. padding: 0 6px;
  434. background: #ffffff;
  435. box-shadow: 0px 2px 4px 0px rgba(102, 102, 102, 0.77);
  436. border-radius: 11px;
  437. text-align: center;
  438. line-height: 22px;
  439. font-weight: 500;
  440. font-size: 15px;
  441. color: #ff8057;
  442. }
  443. }
  444. }
  445. }
  446. }
  447. .rightPlayController {
  448. display: flex;
  449. align-items: center;
  450. .videoName {
  451. font-weight: 500;
  452. font-size: 20px;
  453. color: #ffffff;
  454. }
  455. }
  456. }
  457. }
  458. }
  459. </style>