video-play.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689
  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. imagePos: {
  41. type: String,
  42. default: 'left'
  43. }
  44. },
  45. emits: [
  46. 'canplay',
  47. 'pause',
  48. 'togglePlay',
  49. 'ended',
  50. 'reset',
  51. 'error',
  52. 'close',
  53. 'loadedmetadata'
  54. ],
  55. setup(props, { emit, expose }) {
  56. const { item, isEmtry } = toRefs(props);
  57. const videoFroms = reactive({
  58. paused: true,
  59. speedInKbps: '',
  60. currentTimeNum: 0,
  61. currentTime: '00:00',
  62. durationNum: 0,
  63. duration: '00:00',
  64. showBar: true,
  65. showAction: true,
  66. loop: false,
  67. speedControl: false,
  68. speedStyle: {
  69. left: '1px'
  70. },
  71. defaultSpeed: 1 // 默认速度
  72. });
  73. const videoRef = ref();
  74. const videoItem = ref();
  75. const videoID = ref('video' + Date.now() + Math.floor(Math.random() * 100));
  76. // 对时间进行格式化
  77. const timeFormat = (num: number) => {
  78. if (num > 0) {
  79. const m = Math.floor(num / 60);
  80. const s = num % 60;
  81. return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
  82. } else {
  83. return '00:00';
  84. }
  85. };
  86. // 如果视屏异常后,需要重新播放视屏
  87. const onPlay = () => {
  88. if (videoItem.value) {
  89. videoItem.value.src(item.value.content);
  90. emit('reset');
  91. }
  92. };
  93. //
  94. const toggleHideControl = (isShow: false) => {
  95. videoFroms.showBar = isShow;
  96. videoFroms.speedControl = false;
  97. };
  98. const onReplay = () => {
  99. videoFroms.speedControl = false;
  100. if (!videoItem.value) return;
  101. videoItem.value.currentTime(0);
  102. };
  103. // 切换音频播放
  104. const onToggleVideo = (e?: MouseEvent) => {
  105. e?.stopPropagation();
  106. if (videoFroms.paused) {
  107. videoItem.value.play();
  108. videoFroms.paused = false;
  109. } else {
  110. videoItem.value.pause();
  111. videoFroms.paused = true;
  112. }
  113. emit('togglePlay', videoFroms.paused);
  114. };
  115. const videoTimer = null as any;
  116. let videoTimerErrorCount = 0;
  117. const handlePlayVideo = () => {
  118. if (videoTimerErrorCount > 5) {
  119. return;
  120. }
  121. clearTimeout(videoTimer);
  122. nextTick(() => {
  123. videoItem.value?.play().catch((err: any) => {
  124. // console.log('🚀 ~ err:', err)
  125. // videoTimer = setTimeout(() => {
  126. // if (err?.message?.includes('play()')) {
  127. // // emit('play');
  128. // }
  129. // handlePlayVideo();
  130. // }, 1000);
  131. });
  132. });
  133. videoTimerErrorCount++;
  134. };
  135. const __init = () => {
  136. if (videoItem.value) {
  137. videoItem.value.poster(props.item.coverImg); // 封面
  138. videoItem.value.src(item.value.content + '?t=4'); // url 播放地址
  139. videoItem.value.playbackRate(videoFroms.defaultSpeed);
  140. // 初步加载时
  141. videoItem.value.one('loadedmetadata', () => {
  142. // console.log(' Loading metadata');
  143. videoItem.value.playbackRate(videoFroms.defaultSpeed);
  144. // 获取时长
  145. videoFroms.duration = timeFormat(
  146. Math.round(videoItem.value.duration())
  147. );
  148. videoFroms.durationNum = videoItem.value.duration();
  149. emit('canplay');
  150. emit('loadedmetadata', videoItem.value);
  151. if (item.value.autoPlay && videoItem.value) {
  152. // videoItem.value?.play()
  153. nextTick(() => {
  154. videoTimerErrorCount = 0;
  155. videoItem.value.currentTime(0);
  156. nextTick(handlePlayVideo);
  157. });
  158. }
  159. });
  160. // 视频开始播放
  161. videoItem.value.on('play', () => {
  162. emit('close');
  163. emit('canplay');
  164. });
  165. // 视频播放时加载
  166. videoItem.value.on('timeupdate', () => {
  167. videoFroms.currentTime = timeFormat(
  168. Math.round(videoItem.value?.currentTime() || 0)
  169. );
  170. videoFroms.currentTimeNum = videoItem.value.currentTime();
  171. });
  172. // 视频播放结束
  173. videoItem.value.on('ended', () => {
  174. videoFroms.paused = true;
  175. emit('ended');
  176. });
  177. //
  178. videoItem.value.on('pause', () => {
  179. videoFroms.paused = true;
  180. emit('pause');
  181. });
  182. videoItem.value.on('playing', () => {
  183. videoFroms.paused = false;
  184. });
  185. videoItem.value.on('canplay', (e: any) => {
  186. // 获取时长
  187. videoFroms.duration = timeFormat(
  188. Math.round(videoItem.value.duration())
  189. );
  190. videoFroms.durationNum = videoItem.value.duration();
  191. emit('canplay');
  192. });
  193. // 视频播放异常
  194. videoItem.value.on('error', (e: any) => {
  195. emit('error');
  196. console.log(e, 'error');
  197. // element.pause();
  198. videoItem.value?.pause();
  199. });
  200. }
  201. };
  202. const calculateSpeed = (element: any) => {
  203. let previousBytesLoaded = 0;
  204. let timer: any = null;
  205. let previousTime = Date.now();
  206. let isWaiting = false;
  207. // 缓存检测状态
  208. let isBuffering = false;
  209. // 缓存检测计时器
  210. let bufferTimeout: any = null;
  211. // 设定一个检测缓存停止的时间间隔,这里我们设置为2500毫秒(2秒)
  212. const BUFFER_CHECK_INTERVAL = 2500;
  213. function resetDownloadSpeed(time = 1500) {
  214. timer = setTimeout(() => {
  215. // displayElement.textContent = `视屏下载速度: 0 KB/s`;
  216. videoFroms.speedInKbps = `0 KB/s`;
  217. }, time);
  218. }
  219. function buffterCatch() {
  220. // 设定一个计时器,检查是否在指定的时间内再次触发了progress事件
  221. bufferTimeout = setTimeout(() => {
  222. if (isBuffering) {
  223. // 如果计时器到达且isBuffering仍为true,则认为缓存停止
  224. console.log('停止缓存数据。');
  225. isBuffering = false;
  226. videoFroms.speedInKbps = '';
  227. }
  228. }, BUFFER_CHECK_INTERVAL);
  229. }
  230. element.addEventListener('progress', () => {
  231. const currentTime = Date.now();
  232. const buffered = element.buffered;
  233. let currentBytesLoaded = 0;
  234. if (buffered.length > 0) {
  235. for (let i = 0; i < buffered.length; i++) {
  236. currentBytesLoaded += buffered.end(i) - buffered.start(i);
  237. }
  238. currentBytesLoaded *= element.duration * element.seekable.end(0); // 更精确地近似字节加载量
  239. }
  240. console.log(
  241. 'progress',
  242. currentBytesLoaded > previousBytesLoaded,
  243. '------ 加载中'
  244. );
  245. if (currentBytesLoaded > previousBytesLoaded) {
  246. const timeDiff = (currentTime - previousTime) / 1000; // 时间差转换为秒
  247. const bytesDiff = currentBytesLoaded - previousBytesLoaded; // 字节差值
  248. const speed = bytesDiff / timeDiff; // 字节每秒
  249. console.log(timeDiff, bytesDiff, speed);
  250. console.log(element.paused, 'element.paused');
  251. if (!element.paused) {
  252. const kbps = speed / 1024;
  253. const speedInKbps = kbps.toFixed(2); // 转换为千字节每秒并保留两位小数
  254. if (kbps > 1024) {
  255. videoFroms.speedInKbps = `${Number(
  256. (kbps / 1024).toFixed(2)
  257. )} M/s`;
  258. } else {
  259. videoFroms.speedInKbps = `${Number(speedInKbps)} KB/s`;
  260. }
  261. }
  262. previousBytesLoaded = currentBytesLoaded;
  263. previousTime = currentTime;
  264. }
  265. if (!element.paused) {
  266. // 如果1秒钟没有返回就重置数据
  267. clearTimeout(timer);
  268. resetDownloadSpeed();
  269. }
  270. if (!isWaiting) {
  271. // 如果有缓存检测计时器,则清除它
  272. if (bufferTimeout) {
  273. clearTimeout(bufferTimeout);
  274. }
  275. // 标记为正在缓存
  276. isBuffering = true;
  277. buffterCatch();
  278. }
  279. });
  280. element.addEventListener('waiting', () => {
  281. console.log('waiting');
  282. isWaiting = true;
  283. if (!element.paused) {
  284. // 如果1秒钟没有返回就重置数据
  285. clearTimeout(timer);
  286. resetDownloadSpeed();
  287. }
  288. // 如果有缓存检测计时器,则清除它
  289. if (bufferTimeout) {
  290. clearTimeout(bufferTimeout);
  291. }
  292. });
  293. element.addEventListener('canplay', () => {
  294. console.log('canplay');
  295. isWaiting = false;
  296. // 如果有缓存检测计时器,则清除它
  297. if (bufferTimeout) {
  298. clearTimeout(bufferTimeout);
  299. }
  300. // 标记为正在缓存
  301. isBuffering = true;
  302. buffterCatch();
  303. });
  304. element.addEventListener('pause', () => {
  305. clearTimeout(timer);
  306. // 如果有缓存检测计时器,则清除它
  307. if (bufferTimeout) {
  308. clearTimeout(bufferTimeout);
  309. }
  310. videoFroms.speedInKbps = '';
  311. });
  312. element.addEventListener('error', () => {
  313. element.pause();
  314. videoItem.value?.pause();
  315. });
  316. };
  317. onMounted(() => {
  318. videoItem.value = TCPlayer(videoID.value, {
  319. appID: '',
  320. controls: false
  321. }); // player-container-id 为播放器容器 ID,必须与 html 中一致
  322. __init();
  323. nextTick(() => {
  324. calculateSpeed(videoRef.value);
  325. });
  326. });
  327. const stop = () => {
  328. videoItem.value.currentTime(0);
  329. videoItem.value.pause();
  330. };
  331. const pause = () => {
  332. videoItem.value.pause();
  333. };
  334. onUnmounted(() => {
  335. if (videoItem.value) {
  336. videoItem.value.pause();
  337. videoItem.value.src('');
  338. videoItem.value.dispose();
  339. }
  340. });
  341. watch(
  342. () => props.item,
  343. () => {
  344. // console.log(item.value, 'value----');
  345. videoItem.value.pause();
  346. videoItem.value.currentTime(0);
  347. if (item.value?.id) {
  348. // videoItem.value.poster(props.item.coverImg); // 封面
  349. // videoItem.value.src(item.value.content); // url 播放地址
  350. __init();
  351. videoFroms.paused = true;
  352. }
  353. }
  354. );
  355. watch(
  356. () => props.showModel,
  357. () => {
  358. // console.log(props.showModel, 'props.showModel')
  359. videoFroms.showAction = props.showModel;
  360. videoFroms.speedControl = false;
  361. }
  362. );
  363. expose({
  364. onPlay,
  365. stop,
  366. pause,
  367. // changePlayBtn,
  368. toggleHideControl
  369. });
  370. return () => (
  371. <div class={styles.videoWrap}>
  372. <video
  373. style={{ width: '100%', height: '100%' }}
  374. ref={videoRef}
  375. id={videoID.value}
  376. preload="auto"
  377. playsinline
  378. webkit-playsinline
  379. x5-video-player-type="h5"></video>
  380. <div class={styles.videoPop}></div>
  381. <div
  382. class={[
  383. styles.controls,
  384. videoFroms.showAction ? '' : styles.sectionAnimate
  385. ]}
  386. onClick={(e: MouseEvent) => {
  387. e.stopPropagation();
  388. if (videoItem.value.paused()) return;
  389. emit('close');
  390. emit('reset');
  391. }}>
  392. <div class={styles.slider}>
  393. <NSlider
  394. value={videoFroms.currentTimeNum}
  395. step={0.01}
  396. max={videoFroms.durationNum}
  397. tooltip={false}
  398. onUpdate:value={(val: number) => {
  399. videoFroms.speedControl = false;
  400. videoItem.value.currentTime(val);
  401. videoFroms.currentTimeNum = val;
  402. videoFroms.currentTime = timeFormat(Math.round(val || 0));
  403. }}
  404. />
  405. </div>
  406. <div class={styles.tools}>
  407. {props.imagePos === 'right' ? (
  408. <>
  409. <div class={styles.actions}>
  410. <div class={styles.actionWrap}>
  411. <div class={styles.time}>
  412. <div
  413. class="plyr__time plyr__time--current"
  414. aria-label="Current time">
  415. {videoFroms.currentTime}
  416. </div>
  417. <span class={styles.line}>/</span>
  418. <div
  419. class="plyr__time plyr__time--duration"
  420. aria-label="Duration">
  421. {videoFroms.duration}
  422. </div>
  423. </div>
  424. </div>
  425. </div>
  426. <div class={styles.actions}>
  427. <div class={styles.actionWrap}>
  428. <div
  429. class={styles.downloadSpeed}
  430. style={{ paddingRight: '20px' }}>
  431. {videoFroms.speedInKbps}
  432. </div>
  433. <div
  434. class={styles.actionBtnSpeed}
  435. onClick={e => {
  436. e.stopPropagation();
  437. videoFroms.speedControl = !videoFroms.speedControl;
  438. }}>
  439. <img src={iconSpeed} />
  440. <div
  441. style={{
  442. display: videoFroms.speedControl ? 'block' : 'none'
  443. }}>
  444. <div
  445. class={styles.sliderPopup}
  446. onClick={(e: Event) => {
  447. e.stopPropagation();
  448. }}>
  449. <i
  450. class={styles.iconAdd}
  451. onClick={() => {
  452. if (videoFroms.defaultSpeed >= 1.5) {
  453. return;
  454. }
  455. if (videoItem.value) {
  456. videoFroms.defaultSpeed =
  457. (videoFroms.defaultSpeed * 10 + 1) / 10;
  458. videoItem.value.playbackRate(
  459. videoFroms.defaultSpeed
  460. );
  461. }
  462. }}></i>
  463. <NSlider
  464. value={videoFroms.defaultSpeed}
  465. step={0.1}
  466. max={1.5}
  467. min={0.5}
  468. vertical
  469. tooltip={false}
  470. onUpdate:value={(val: number) => {
  471. videoFroms.defaultSpeed = val;
  472. if (videoItem.value) {
  473. videoItem.value.playbackRate(
  474. videoFroms.defaultSpeed
  475. );
  476. }
  477. }}>
  478. {{
  479. thumb: () => (
  480. <div class={styles.sliderPoint}>
  481. {videoFroms.defaultSpeed}
  482. <span>x</span>
  483. </div>
  484. )
  485. }}
  486. </NSlider>
  487. <i
  488. class={[styles.iconCut]}
  489. onClick={() => {
  490. if (videoFroms.defaultSpeed <= 0.5) {
  491. return;
  492. }
  493. if (videoItem.value) {
  494. videoFroms.defaultSpeed =
  495. (videoFroms.defaultSpeed * 10 - 1) / 10;
  496. videoItem.value.playbackRate(
  497. videoFroms.defaultSpeed
  498. );
  499. }
  500. }}></i>
  501. </div>
  502. </div>
  503. </div>
  504. <button class={styles.iconReplay} onClick={onReplay}>
  505. <img src={iconLoop} />
  506. </button>
  507. <div
  508. class={styles.actionBtn}
  509. onClick={() => {
  510. videoFroms.speedControl = false;
  511. onToggleVideo();
  512. }}>
  513. {videoFroms.paused ? (
  514. <img class={styles.playIcon} src={iconplay} />
  515. ) : (
  516. <img class={styles.playIcon} src={iconpause} />
  517. )}
  518. </div>
  519. </div>
  520. </div>
  521. </>
  522. ) : (
  523. <>
  524. <div class={styles.actions}>
  525. <div class={styles.actionWrap}>
  526. <div
  527. class={styles.actionBtn}
  528. onClick={() => {
  529. videoFroms.speedControl = false;
  530. onToggleVideo();
  531. }}>
  532. {videoFroms.paused ? (
  533. <img class={styles.playIcon} src={iconplay} />
  534. ) : (
  535. <img class={styles.playIcon} src={iconpause} />
  536. )}
  537. </div>
  538. <button class={styles.iconReplay} onClick={onReplay}>
  539. <img src={iconLoop} />
  540. </button>
  541. <div
  542. class={styles.actionBtnSpeed}
  543. onClick={e => {
  544. e.stopPropagation();
  545. videoFroms.speedControl = !videoFroms.speedControl;
  546. }}>
  547. <img src={iconSpeed} />
  548. <div
  549. style={{
  550. display: videoFroms.speedControl ? 'block' : 'none'
  551. }}>
  552. <div
  553. class={styles.sliderPopup}
  554. onClick={(e: Event) => {
  555. e.stopPropagation();
  556. }}>
  557. <i
  558. class={styles.iconAdd}
  559. onClick={() => {
  560. if (videoFroms.defaultSpeed >= 1.5) {
  561. return;
  562. }
  563. if (videoItem.value) {
  564. videoFroms.defaultSpeed =
  565. (videoFroms.defaultSpeed * 10 + 1) / 10;
  566. videoItem.value.playbackRate(
  567. videoFroms.defaultSpeed
  568. );
  569. }
  570. }}></i>
  571. <NSlider
  572. value={videoFroms.defaultSpeed}
  573. step={0.1}
  574. max={1.5}
  575. min={0.5}
  576. vertical
  577. tooltip={false}
  578. onUpdate:value={(val: number) => {
  579. videoFroms.defaultSpeed = val;
  580. if (videoItem.value) {
  581. videoItem.value.playbackRate(
  582. videoFroms.defaultSpeed
  583. );
  584. }
  585. }}>
  586. {{
  587. thumb: () => (
  588. <div class={styles.sliderPoint}>
  589. {videoFroms.defaultSpeed}
  590. <span>x</span>
  591. </div>
  592. )
  593. }}
  594. </NSlider>
  595. <i
  596. class={[styles.iconCut]}
  597. onClick={() => {
  598. if (videoFroms.defaultSpeed <= 0.5) {
  599. return;
  600. }
  601. if (videoItem.value) {
  602. videoFroms.defaultSpeed =
  603. (videoFroms.defaultSpeed * 10 - 1) / 10;
  604. videoItem.value.playbackRate(
  605. videoFroms.defaultSpeed
  606. );
  607. }
  608. }}></i>
  609. </div>
  610. </div>
  611. </div>
  612. <div class={styles.downloadSpeed}>
  613. {videoFroms.speedInKbps}
  614. </div>
  615. </div>
  616. </div>
  617. <div class={styles.actions}>
  618. <div class={styles.actionWrap}>
  619. <div class={styles.time}>
  620. <div
  621. class="plyr__time plyr__time--current"
  622. aria-label="Current time">
  623. {videoFroms.currentTime}
  624. </div>
  625. <span class={styles.line}>/</span>
  626. <div
  627. class="plyr__time plyr__time--duration"
  628. aria-label="Duration">
  629. {videoFroms.duration}
  630. </div>
  631. </div>
  632. </div>
  633. </div>
  634. </>
  635. )}
  636. </div>
  637. </div>
  638. </div>
  639. );
  640. }
  641. });