video-play.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. import {
  2. defineComponent,
  3. nextTick,
  4. onMounted,
  5. reactive,
  6. toRefs,
  7. watch
  8. } from 'vue';
  9. import { ref } from 'vue';
  10. import styles from './video.module.less';
  11. // import iconLoop from '../image/icon-loop.svg';
  12. // import iconLoopActive from '../image/icon-loop-active.svg';
  13. // import iconplay from '../image/icon-play.svg';
  14. // import iconpause from '../image/icon-pause.svg';
  15. import {
  16. // iconVideoBg,
  17. iconLoop,
  18. iconLoopActive,
  19. iconPlay,
  20. iconPause,
  21. iconSpeed
  22. } from '../image/icons.json';
  23. import TCPlayer from 'tcplayer.js';
  24. import 'tcplayer.js/dist/tcplayer.min.css';
  25. import { Slider } from 'vant';
  26. // 秒转分
  27. export const getSecondRPM = (second: number, type?: string) => {
  28. if (isNaN(second)) return '00:00';
  29. const mm = Math.floor(second / 60)
  30. .toString()
  31. .padStart(2, '0');
  32. const dd = Math.floor(second % 60)
  33. .toString()
  34. .padStart(2, '0');
  35. if (type === 'cn') {
  36. return mm + '分' + dd + '秒';
  37. } else {
  38. return mm + ':' + dd;
  39. }
  40. };
  41. export default defineComponent({
  42. name: 'video-play',
  43. props: {
  44. item: {
  45. type: Object,
  46. default: () => {
  47. return {};
  48. }
  49. },
  50. isEmtry: {
  51. type: Boolean,
  52. default: false
  53. },
  54. isActive: {
  55. type: Boolean,
  56. default: false
  57. },
  58. activeModel: {
  59. type: Boolean,
  60. default: true
  61. }
  62. },
  63. emits: [
  64. 'loadedmetadata',
  65. 'togglePlay',
  66. 'ended',
  67. 'reset',
  68. 'error',
  69. 'close',
  70. 'play',
  71. 'pause',
  72. 'seeked',
  73. 'seeking',
  74. 'waiting',
  75. 'timeupdate'
  76. ],
  77. setup(props, { emit, expose }) {
  78. const { item } = toRefs(props);
  79. const data = reactive({
  80. timer: null as any,
  81. currentTime: 0,
  82. duration: 0.1,
  83. loop: false,
  84. playState: 'pause' as 'play' | 'pause',
  85. vudio: null as any,
  86. showBar: true,
  87. speedControl: false,
  88. speedStyle: {
  89. left: '1px'
  90. },
  91. defaultSpeed: 1 // 默认速度
  92. });
  93. const speedBtnId = 'speed' + Date.now() + Math.floor(Math.random() * 100);
  94. // const forms = reactive({
  95. // subjectIds: [],
  96. // orgainIds: []
  97. // });
  98. const videoRef = ref();
  99. const videoItem = ref();
  100. const videoID = 'video' + Date.now() + Math.floor(Math.random() * 100);
  101. const toggleHideControl = (isShow: boolean) => {
  102. data.speedControl = false;
  103. data.showBar = isShow;
  104. };
  105. // const togglePlay = (e: Event) => {
  106. // e.stopPropagation()
  107. // }
  108. let playTimer = null as any;
  109. // 切换音频播放
  110. const onToggleAudio = (state: 'play' | 'pause') => {
  111. // console.log(state, 'state')
  112. data.speedControl = false;
  113. clearTimeout(playTimer);
  114. if (state === 'play') {
  115. playTimer = setTimeout(() => {
  116. videoItem.value?.play();
  117. data.playState = 'play';
  118. }, 100);
  119. } else {
  120. videoItem.value?.pause();
  121. data.playState = 'pause';
  122. }
  123. emit('togglePlay', data.playState);
  124. };
  125. const toggleLoop = () => {
  126. data.speedControl = false;
  127. if (!videoItem.value) return;
  128. if (data.loop) {
  129. videoItem.value.loop(false);
  130. } else {
  131. videoItem.value.loop(true);
  132. }
  133. data.loop = !data.loop;
  134. };
  135. const changePlayBtn = (code: string) => {
  136. // data.speedControl = false;
  137. if (code == 'play') {
  138. data.playState = 'play';
  139. } else {
  140. data.playState = 'pause';
  141. }
  142. };
  143. /** 改变播放时间 */
  144. const handleChangeTime = (val: number) => {
  145. data.currentTime = val;
  146. clearTimeout(data.timer);
  147. data.timer = setTimeout(() => {
  148. videoItem.value.currentTime(val);
  149. data.timer = null;
  150. }, 300);
  151. };
  152. const __initVideo = () => {
  153. if (videoItem.value && props.item.id) {
  154. nextTick(() => {
  155. videoItem.value?.currentTime(0);
  156. });
  157. videoItem.value.poster(props.item.coverImg); // 封面
  158. videoItem.value.src(props.item.content); // url 播放地址
  159. videoItem.value.playbackRate(data.defaultSpeed);
  160. // 初步加载时
  161. videoItem.value.on('loadedmetadata', () => {
  162. videoItem.value.playbackRate(data.defaultSpeed);
  163. // 获取时长
  164. data.duration = videoItem.value.duration();
  165. // 必须在当前元素
  166. if (item.value.autoPlay && videoItem.value && props.isActive) {
  167. // videoItem.value?.play()
  168. nextTick(() => {
  169. videoItem.value.currentTime(0);
  170. nextTick(handlePlayVideo);
  171. });
  172. }
  173. emit('loadedmetadata', videoItem.value);
  174. });
  175. // 视频播放时加载
  176. videoItem.value.on('timeupdate', () => {
  177. if (data.timer) return;
  178. data.currentTime = videoItem.value.currentTime();
  179. emit('timeupdate');
  180. });
  181. // 视频播放结束
  182. videoItem.value.on('ended', () => {
  183. changePlayBtn('pause');
  184. emit('ended');
  185. });
  186. //
  187. videoItem.value.on('pause', () => {
  188. data.playState = 'pause';
  189. changePlayBtn('pause');
  190. emit('togglePlay', true);
  191. emit('pause');
  192. });
  193. videoItem.value.on('seeked', () => {
  194. emit('seeked');
  195. });
  196. videoItem.value.on('seeking', () => {
  197. emit('seeking');
  198. });
  199. videoItem.value.on('waiting', () => {
  200. emit('waiting');
  201. });
  202. videoItem.value.on('play', () => {
  203. // console.log(play, 'playing')
  204. changePlayBtn('play');
  205. if (videoItem.value) {
  206. videoItem.value.muted(false);
  207. videoItem.value.volume(1);
  208. }
  209. if (
  210. !item.value.autoPlay &&
  211. !item.value.isprepare &&
  212. videoItem.value
  213. ) {
  214. // 加载完成后,取消静音播放
  215. videoItem.value.pause();
  216. }
  217. emit('togglePlay', videoItem.value?.paused);
  218. emit('play');
  219. });
  220. // 视频播放异常
  221. videoItem.value.on('error', (e: any) => {
  222. handleErrorVideo();
  223. emit('error');
  224. console.log(e, 'error');
  225. });
  226. }
  227. };
  228. let videoTimer = null as any;
  229. let videoTimerErrorCount = 0;
  230. const handlePlayVideo = () => {
  231. if (videoTimerErrorCount > 5) {
  232. return;
  233. }
  234. clearTimeout(videoTimer);
  235. nextTick(() => {
  236. videoItem.value?.play().catch((err: any) => {
  237. // console.log('🚀 ~ err:', err)
  238. videoTimer = setTimeout(() => {
  239. if (err?.message?.includes('play()')) {
  240. emit('play');
  241. }
  242. handlePlayVideo();
  243. }, 1000);
  244. });
  245. });
  246. videoTimerErrorCount++;
  247. };
  248. let videoErrorTimer = null as any;
  249. let videoErrorCount = 0;
  250. const handleErrorVideo = () => {
  251. if (videoErrorCount > 5) {
  252. return;
  253. }
  254. clearTimeout(videoErrorTimer);
  255. nextTick(() => {
  256. videoErrorTimer = setTimeout(() => {
  257. videoItem.value.src = props.item?.content;
  258. emit('play');
  259. videoItem.value.load();
  260. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  261. handleErrorVideo();
  262. }, 1000);
  263. });
  264. videoErrorCount++;
  265. };
  266. onMounted(() => {
  267. videoItem.value = TCPlayer(videoID, {
  268. appID: '',
  269. controls: false,
  270. autoplay: true
  271. }); // player-container-id 为播放器容器 ID,必须与 html 中一致
  272. __initVideo();
  273. document.getElementById(speedBtnId)?.addEventListener('click', e => {
  274. e.stopPropagation();
  275. data.speedControl = !data.speedControl;
  276. });
  277. });
  278. watch(
  279. () => props.activeModel,
  280. () => {
  281. toggleHideControl(props.activeModel);
  282. }
  283. );
  284. watch(
  285. () => props.item,
  286. () => {
  287. videoItem.value?.currentTime(0);
  288. videoItem.value?.pause();
  289. setTimeout(() => {
  290. __initVideo();
  291. }, 60);
  292. }
  293. );
  294. const getVideoRef = () => {
  295. return videoRef.value;
  296. };
  297. const getPlyrRef = () => {
  298. return videoItem.value;
  299. };
  300. expose({
  301. changePlayBtn,
  302. toggleHideControl,
  303. getVideoRef,
  304. getPlyrRef
  305. });
  306. // watch(
  307. // () => props.isActive,
  308. // val => {
  309. // if (!val) {
  310. // videoItem.value?.pause();
  311. // }
  312. // }
  313. // );
  314. return () => (
  315. <div
  316. class={styles.videoWrap}
  317. onClick={() => {
  318. data.speedControl = false;
  319. }}>
  320. <video
  321. style={{ width: '100%', height: '100%' }}
  322. src={item.value.content}
  323. ref={videoRef}
  324. id={videoID}
  325. preload="auto"
  326. playsinline
  327. webkit-playsinline></video>
  328. <div class={styles.videoSection}></div>
  329. <div
  330. class={[styles.controls, data.showBar ? '' : styles.hide]}
  331. onClick={(e: Event) => {
  332. e.stopPropagation();
  333. }}
  334. // onTouchmove={(e: TouchEvent) => {
  335. // emit('close')
  336. // }}
  337. >
  338. <div class={styles.time}>
  339. <div>{getSecondRPM(data.currentTime)}</div>
  340. <div>{getSecondRPM(data.duration)}</div>
  341. </div>
  342. <div class={styles.slider}>
  343. <Slider
  344. step={0.01}
  345. class={styles.timeProgress}
  346. v-model={data.currentTime}
  347. max={data.duration}
  348. onUpdate:modelValue={val => {
  349. handleChangeTime(val);
  350. }}
  351. />
  352. </div>
  353. <div class={styles.actionSection}>
  354. <div class={styles.actions} onClick={() => emit('close')}>
  355. <div
  356. class={styles.actionBtn}
  357. onClick={(e: any) => {
  358. e.stopPropagation();
  359. onToggleAudio(data.playState === 'pause' ? 'play' : 'pause');
  360. }}>
  361. <img src={data.playState === 'pause' ? iconPlay : iconPause} />
  362. </div>
  363. <div class={styles.actionBtn} onClick={toggleLoop}>
  364. <img src={data.loop ? iconLoopActive : iconLoop} />
  365. </div>
  366. <div class={styles.actionBtn} id={speedBtnId}>
  367. <img src={iconSpeed} />
  368. </div>
  369. </div>
  370. <div class={styles.name}>{item.value.name}</div>
  371. </div>
  372. </div>
  373. <div
  374. style={{
  375. display: data.speedControl ? 'block' : 'none'
  376. }}>
  377. <div
  378. class={styles.sliderPopup}
  379. onClick={(e: Event) => {
  380. e.stopPropagation();
  381. }}>
  382. <i
  383. class={styles.iconAdd}
  384. onClick={() => {
  385. if (data.defaultSpeed >= 1.5) {
  386. return;
  387. }
  388. if (videoItem.value) {
  389. data.defaultSpeed = (data.defaultSpeed * 10 + 1) / 10;
  390. videoItem.value.playbackRate(data.defaultSpeed);
  391. }
  392. }}></i>
  393. <Slider
  394. min={0.5}
  395. max={1.5}
  396. step={0.1}
  397. v-model={data.defaultSpeed}
  398. vertical
  399. barHeight={5}
  400. reverse
  401. onChange={() => {
  402. if (videoItem.value) {
  403. videoItem.value.playbackRate(data.defaultSpeed);
  404. }
  405. }}>
  406. {{
  407. button: () => (
  408. <div class={styles.sliderPoint}>
  409. {data.defaultSpeed}
  410. <span>x</span>
  411. </div>
  412. )
  413. }}
  414. </Slider>
  415. <i
  416. class={[styles.iconCut]}
  417. onClick={() => {
  418. if (data.defaultSpeed <= 0.5) {
  419. return;
  420. }
  421. if (videoItem.value) {
  422. data.defaultSpeed = (data.defaultSpeed * 10 - 1) / 10;
  423. videoItem.value.playbackRate(data.defaultSpeed);
  424. }
  425. }}></i>
  426. </div>
  427. </div>
  428. </div>
  429. );
  430. }
  431. });