video-play.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. import {
  2. defineComponent,
  3. nextTick,
  4. onMounted,
  5. onUnmounted,
  6. reactive,
  7. toRefs,
  8. watch
  9. } from 'vue';
  10. import TCPlayer from 'tcplayer.js';
  11. import 'tcplayer.js/dist/tcplayer.min.css';
  12. // import 'plyr/dist/plyr.css';
  13. // import Plyr from 'plyr';
  14. import { ref } from 'vue';
  15. import styles from './video.module.less';
  16. import iconplay from '../image/icon-pause.png';
  17. import iconpause from '../image/icon-play.png';
  18. // import iconReplay from '../image/icon-replay.png';
  19. import iconLoop from '../image/icon-loop.svg';
  20. import iconLoopActive from '../image/icon-loop-active.svg';
  21. import iconSpeed from '../image/icon-speed.png';
  22. import { NSlider } from 'naive-ui';
  23. export default defineComponent({
  24. name: 'video-play',
  25. props: {
  26. item: {
  27. type: Object,
  28. default: () => {
  29. return {};
  30. }
  31. },
  32. showModel: {
  33. type: Boolean,
  34. default: false
  35. },
  36. isEmtry: {
  37. type: Boolean,
  38. default: false
  39. }
  40. },
  41. emits: [
  42. 'canplay',
  43. 'pause',
  44. 'togglePlay',
  45. 'ended',
  46. 'reset',
  47. 'error',
  48. 'close',
  49. 'loadedmetadata'
  50. ],
  51. setup(props, { emit, expose }) {
  52. const { item, isEmtry } = toRefs(props);
  53. const videoFroms = reactive({
  54. paused: true,
  55. currentTimeNum: 0,
  56. currentTime: '00:00',
  57. durationNum: 0,
  58. duration: '00:00',
  59. showBar: true,
  60. showAction: true,
  61. loop: false,
  62. speedControl: false,
  63. speedStyle: {
  64. left: '1px'
  65. },
  66. defaultSpeed: 1 // 默认速度
  67. });
  68. const videoRef = ref();
  69. const videoItem = ref();
  70. const videoID = ref('video' + Date.now() + Math.floor(Math.random() * 100));
  71. const speedBtnId = 'speed' + Date.now() + Math.floor(Math.random() * 100);
  72. // 对时间进行格式化
  73. const timeFormat = (num: number) => {
  74. if (num > 0) {
  75. const m = Math.floor(num / 60);
  76. const s = num % 60;
  77. return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
  78. } else {
  79. return '00:00';
  80. }
  81. };
  82. // 如果视屏异常后,需要重新播放视屏
  83. const onPlay = () => {
  84. if (videoItem.value) {
  85. videoItem.value.src(item.value.content);
  86. emit('reset');
  87. }
  88. };
  89. //
  90. const toggleHideControl = (isShow: false) => {
  91. videoFroms.showBar = isShow;
  92. videoFroms.speedControl = false;
  93. };
  94. const onReplay = () => {
  95. videoFroms.speedControl = false;
  96. if (!videoItem.value) return;
  97. videoItem.value.currentTime(0);
  98. };
  99. // 切换音频播放
  100. const onToggleVideo = (e?: MouseEvent) => {
  101. e?.stopPropagation();
  102. if (videoFroms.paused) {
  103. videoItem.value.play();
  104. videoFroms.paused = false;
  105. } else {
  106. videoItem.value.pause();
  107. videoFroms.paused = true;
  108. }
  109. emit('togglePlay', videoFroms.paused);
  110. };
  111. let videoTimer = null as any;
  112. let videoTimerErrorCount = 0;
  113. const handlePlayVideo = () => {
  114. if (videoTimerErrorCount > 5) {
  115. return;
  116. }
  117. clearTimeout(videoTimer);
  118. nextTick(() => {
  119. videoItem.value?.play().catch((err: any) => {
  120. // console.log('🚀 ~ err:', err)
  121. videoTimer = setTimeout(() => {
  122. if (err?.message?.includes('play()')) {
  123. // emit('play');
  124. }
  125. handlePlayVideo();
  126. }, 1000);
  127. });
  128. });
  129. videoTimerErrorCount++;
  130. };
  131. const __init = () => {
  132. if (videoItem.value) {
  133. videoItem.value.poster(props.item.coverImg); // 封面
  134. videoItem.value.src(item.value.content); // url 播放地址
  135. videoItem.value.playbackRate(videoFroms.defaultSpeed);
  136. // 初步加载时
  137. videoItem.value.one('loadedmetadata', () => {
  138. // console.log(' Loading metadata');
  139. videoItem.value.playbackRate(videoFroms.defaultSpeed);
  140. // 获取时长
  141. videoFroms.duration = timeFormat(
  142. Math.round(videoItem.value.duration())
  143. );
  144. videoFroms.durationNum = videoItem.value.duration();
  145. emit('canplay');
  146. emit('loadedmetadata', videoItem.value);
  147. if (item.value.autoPlay && videoItem.value) {
  148. // videoItem.value?.play()
  149. nextTick(() => {
  150. videoTimerErrorCount = 0;
  151. videoItem.value.currentTime(0);
  152. nextTick(handlePlayVideo);
  153. });
  154. }
  155. });
  156. // 视频开始播放
  157. videoItem.value.on('play', () => {
  158. emit('close');
  159. emit('canplay');
  160. });
  161. // 视频播放时加载
  162. videoItem.value.on('timeupdate', () => {
  163. videoFroms.currentTime = timeFormat(
  164. Math.round(videoItem.value?.currentTime() || 0)
  165. );
  166. videoFroms.currentTimeNum = videoItem.value.currentTime();
  167. });
  168. // 视频播放结束
  169. videoItem.value.on('ended', () => {
  170. videoFroms.paused = true;
  171. emit('ended');
  172. });
  173. //
  174. videoItem.value.on('pause', () => {
  175. videoFroms.paused = true;
  176. emit('pause');
  177. });
  178. videoItem.value.on('playing', () => {
  179. videoFroms.paused = false;
  180. });
  181. videoItem.value.on('canplay', (e: any) => {
  182. // 获取时长
  183. videoFroms.duration = timeFormat(
  184. Math.round(videoItem.value.duration())
  185. );
  186. videoFroms.durationNum = videoItem.value.duration();
  187. emit('canplay');
  188. });
  189. // 视频播放异常
  190. videoItem.value.on('error', (e: any) => {
  191. emit('error');
  192. console.log(e, 'error');
  193. });
  194. }
  195. };
  196. onMounted(() => {
  197. videoItem.value = TCPlayer(videoID.value, {
  198. appID: '',
  199. controls: false
  200. }); // player-container-id 为播放器容器 ID,必须与 html 中一致
  201. __init();
  202. document.getElementById(speedBtnId)?.addEventListener('click', e => {
  203. e.stopPropagation();
  204. videoFroms.speedControl = !videoFroms.speedControl;
  205. });
  206. });
  207. const stop = () => {
  208. videoItem.value.currentTime(0);
  209. videoItem.value.pause();
  210. };
  211. const pause = () => {
  212. videoItem.value.pause();
  213. };
  214. onUnmounted(() => {
  215. if (videoItem.value) {
  216. videoItem.value.pause();
  217. videoItem.value.src('');
  218. videoItem.value.dispose();
  219. }
  220. });
  221. watch(
  222. () => props.item,
  223. () => {
  224. // console.log(item.value, 'value----');
  225. videoItem.value.pause();
  226. videoItem.value.currentTime(0);
  227. if (item.value?.id) {
  228. // videoItem.value.poster(props.item.coverImg); // 封面
  229. // videoItem.value.src(item.value.content); // url 播放地址
  230. __init();
  231. videoFroms.paused = true;
  232. }
  233. }
  234. );
  235. watch(
  236. () => props.showModel,
  237. () => {
  238. // console.log(props.showModel, 'props.showModel')
  239. videoFroms.showAction = props.showModel;
  240. videoFroms.speedControl = false;
  241. }
  242. );
  243. expose({
  244. onPlay,
  245. stop,
  246. pause,
  247. // changePlayBtn,
  248. toggleHideControl
  249. });
  250. return () => (
  251. <div class={styles.videoWrap}>
  252. <video
  253. style={{ width: '100%', height: '100%' }}
  254. ref={videoRef}
  255. id={videoID.value}
  256. preload="auto"
  257. playsinline
  258. webkit-playsinline
  259. x5-video-player-type="h5"></video>
  260. <div class={styles.videoPop}></div>
  261. <div
  262. class={[
  263. styles.controls,
  264. videoFroms.showAction ? '' : styles.sectionAnimate
  265. ]}
  266. onClick={(e: MouseEvent) => {
  267. e.stopPropagation();
  268. if (videoItem.value.paused()) return;
  269. emit('close');
  270. emit('reset');
  271. }}>
  272. <div class={styles.actions}>
  273. <div class={styles.actionWrap}>
  274. <div
  275. class={styles.actionBtn}
  276. onClick={() => {
  277. videoFroms.speedControl = false;
  278. onToggleVideo();
  279. }}>
  280. {videoFroms.paused ? (
  281. <img class={styles.playIcon} src={iconplay} />
  282. ) : (
  283. <img class={styles.playIcon} src={iconpause} />
  284. )}
  285. </div>
  286. <button class={styles.iconReplay} onClick={onReplay}>
  287. <img src={iconLoop} />
  288. </button>
  289. <div class={styles.actionBtnSpeed} id={speedBtnId}>
  290. <img src={iconSpeed} />
  291. </div>
  292. </div>
  293. </div>
  294. <div class={styles.slider}>
  295. <NSlider
  296. value={videoFroms.currentTimeNum}
  297. step={0.01}
  298. max={videoFroms.durationNum}
  299. tooltip={false}
  300. onUpdate:value={(val: number) => {
  301. videoFroms.speedControl = false;
  302. videoItem.value.currentTime(val);
  303. videoFroms.currentTimeNum = val;
  304. videoFroms.currentTime = timeFormat(Math.round(val || 0));
  305. }}
  306. />
  307. </div>
  308. <div class={styles.actions}>
  309. <div class={styles.actionWrap}>
  310. <div class={styles.time}>
  311. <div
  312. class="plyr__time plyr__time--current"
  313. aria-label="Current time">
  314. {videoFroms.currentTime}
  315. </div>
  316. <span class={styles.line}>/</span>
  317. <div
  318. class="plyr__time plyr__time--duration"
  319. aria-label="Duration">
  320. {videoFroms.duration}
  321. </div>
  322. </div>
  323. </div>
  324. </div>
  325. </div>
  326. <div
  327. style={{
  328. display: videoFroms.speedControl ? 'block' : 'none'
  329. }}>
  330. <div
  331. class={styles.sliderPopup}
  332. onClick={(e: Event) => {
  333. e.stopPropagation();
  334. }}>
  335. <i
  336. class={styles.iconAdd}
  337. onClick={() => {
  338. if (videoFroms.defaultSpeed >= 1.5) {
  339. return;
  340. }
  341. if (videoItem.value) {
  342. videoFroms.defaultSpeed =
  343. (videoFroms.defaultSpeed * 10 + 1) / 10;
  344. videoItem.value.playbackRate(videoFroms.defaultSpeed);
  345. }
  346. }}></i>
  347. <NSlider
  348. value={videoFroms.defaultSpeed}
  349. step={0.1}
  350. max={1.5}
  351. min={0.6}
  352. vertical
  353. tooltip={false}
  354. onUpdate:value={(val: number) => {
  355. videoFroms.defaultSpeed = val;
  356. if (videoItem.value) {
  357. videoItem.value.playbackRate(videoFroms.defaultSpeed);
  358. }
  359. }}>
  360. {{
  361. thumb: () => (
  362. <div class={styles.sliderPoint}>
  363. {videoFroms.defaultSpeed}
  364. <span>x</span>
  365. </div>
  366. )
  367. }}
  368. </NSlider>
  369. <i
  370. class={[styles.iconCut]}
  371. onClick={() => {
  372. if (videoFroms.defaultSpeed <= 0.6) {
  373. return;
  374. }
  375. if (videoItem.value) {
  376. videoFroms.defaultSpeed =
  377. (videoFroms.defaultSpeed * 10 - 1) / 10;
  378. videoItem.value.playbackRate(videoFroms.defaultSpeed);
  379. }
  380. }}></i>
  381. </div>
  382. </div>
  383. </div>
  384. );
  385. }
  386. });