video-play.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  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: '0 KB/s',
  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. });
  198. }
  199. };
  200. const calculateSpeed = (element: any) => {
  201. let previousBytesLoaded = 0;
  202. let timer: any = null;
  203. let previousTime = Date.now();
  204. function resetDownloadSpeed() {
  205. timer = setTimeout(() => {
  206. // displayElement.textContent = `视屏下载速度: 0 KB/s`;
  207. videoFroms.speedInKbps = `0 KB/s`;
  208. }, 2000);
  209. }
  210. element.addEventListener('progress', () => {
  211. const currentTime = Date.now();
  212. const buffered = element.buffered;
  213. let currentBytesLoaded = 0;
  214. if (buffered.length > 0) {
  215. for (let i = 0; i < buffered.length; i++) {
  216. currentBytesLoaded += buffered.end(i) - buffered.start(i);
  217. }
  218. currentBytesLoaded *= element.duration * element.seekable.end(0); // 更精确地近似字节加载量
  219. }
  220. console.log('progress', currentBytesLoaded > previousBytesLoaded);
  221. if (currentBytesLoaded > previousBytesLoaded) {
  222. const timeDiff = (currentTime - previousTime) / 1000; // 时间差转换为秒
  223. const bytesDiff = currentBytesLoaded - previousBytesLoaded; // 字节差值
  224. const speed = bytesDiff / timeDiff; // 字节每秒
  225. console.log(timeDiff, bytesDiff, speed);
  226. const kbps = speed / 1024;
  227. const speedInKbps = kbps.toFixed(2); // 转换为千字节每秒并保留两位小数
  228. if (kbps > 1024) {
  229. videoFroms.speedInKbps = `${Number((kbps / 1024).toFixed(2))} M/s`;
  230. } else {
  231. videoFroms.speedInKbps = `${Number(speedInKbps)} KB/s`;
  232. }
  233. previousBytesLoaded = currentBytesLoaded;
  234. previousTime = currentTime;
  235. // 如果1秒钟没有返回就重置数据
  236. clearTimeout(timer);
  237. resetDownloadSpeed();
  238. }
  239. });
  240. };
  241. onMounted(() => {
  242. videoItem.value = TCPlayer(videoID.value, {
  243. appID: '',
  244. controls: false
  245. }); // player-container-id 为播放器容器 ID,必须与 html 中一致
  246. __init();
  247. nextTick(() => {
  248. calculateSpeed(videoRef.value);
  249. });
  250. });
  251. const stop = () => {
  252. videoItem.value.currentTime(0);
  253. videoItem.value.pause();
  254. };
  255. const pause = () => {
  256. videoItem.value.pause();
  257. };
  258. onUnmounted(() => {
  259. if (videoItem.value) {
  260. videoItem.value.pause();
  261. videoItem.value.src('');
  262. videoItem.value.dispose();
  263. }
  264. });
  265. watch(
  266. () => props.item,
  267. () => {
  268. // console.log(item.value, 'value----');
  269. videoItem.value.pause();
  270. videoItem.value.currentTime(0);
  271. if (item.value?.id) {
  272. // videoItem.value.poster(props.item.coverImg); // 封面
  273. // videoItem.value.src(item.value.content); // url 播放地址
  274. __init();
  275. videoFroms.paused = true;
  276. }
  277. }
  278. );
  279. watch(
  280. () => props.showModel,
  281. () => {
  282. // console.log(props.showModel, 'props.showModel')
  283. videoFroms.showAction = props.showModel;
  284. videoFroms.speedControl = false;
  285. }
  286. );
  287. expose({
  288. onPlay,
  289. stop,
  290. pause,
  291. // changePlayBtn,
  292. toggleHideControl
  293. });
  294. return () => (
  295. <div class={styles.videoWrap}>
  296. <video
  297. style={{ width: '100%', height: '100%' }}
  298. ref={videoRef}
  299. id={videoID.value}
  300. preload="auto"
  301. playsinline
  302. webkit-playsinline
  303. x5-video-player-type="h5"></video>
  304. <div class={styles.videoPop}></div>
  305. <div
  306. class={[
  307. styles.controls,
  308. videoFroms.showAction ? '' : styles.sectionAnimate
  309. ]}
  310. onClick={(e: MouseEvent) => {
  311. e.stopPropagation();
  312. if (videoItem.value.paused()) return;
  313. emit('close');
  314. emit('reset');
  315. }}>
  316. <div class={styles.slider}>
  317. <NSlider
  318. value={videoFroms.currentTimeNum}
  319. step={0.01}
  320. max={videoFroms.durationNum}
  321. tooltip={false}
  322. onUpdate:value={(val: number) => {
  323. videoFroms.speedControl = false;
  324. videoItem.value.currentTime(val);
  325. videoFroms.currentTimeNum = val;
  326. videoFroms.currentTime = timeFormat(Math.round(val || 0));
  327. }}
  328. />
  329. </div>
  330. <div class={styles.tools}>
  331. {props.imagePos === 'right' ? (
  332. <>
  333. <div class={styles.actions}>
  334. <div class={styles.actionWrap}>
  335. <div class={styles.time}>
  336. <div
  337. class="plyr__time plyr__time--current"
  338. aria-label="Current time">
  339. {videoFroms.currentTime}
  340. </div>
  341. <span class={styles.line}>/</span>
  342. <div
  343. class="plyr__time plyr__time--duration"
  344. aria-label="Duration">
  345. {videoFroms.duration}
  346. </div>
  347. </div>
  348. </div>
  349. </div>
  350. <div class={styles.actions}>
  351. <div class={styles.actionWrap}>
  352. <div
  353. class={styles.actionBtnSpeed}
  354. onClick={e => {
  355. e.stopPropagation();
  356. videoFroms.speedControl = !videoFroms.speedControl;
  357. }}>
  358. <img src={iconSpeed} />
  359. <div
  360. style={{
  361. display: videoFroms.speedControl ? 'block' : 'none'
  362. }}>
  363. <div
  364. class={styles.sliderPopup}
  365. onClick={(e: Event) => {
  366. e.stopPropagation();
  367. }}>
  368. <i
  369. class={styles.iconAdd}
  370. onClick={() => {
  371. if (videoFroms.defaultSpeed >= 1.5) {
  372. return;
  373. }
  374. if (videoItem.value) {
  375. videoFroms.defaultSpeed =
  376. (videoFroms.defaultSpeed * 10 + 1) / 10;
  377. videoItem.value.playbackRate(
  378. videoFroms.defaultSpeed
  379. );
  380. }
  381. }}></i>
  382. <NSlider
  383. value={videoFroms.defaultSpeed}
  384. step={0.1}
  385. max={1.5}
  386. min={0.5}
  387. vertical
  388. tooltip={false}
  389. onUpdate:value={(val: number) => {
  390. videoFroms.defaultSpeed = val;
  391. if (videoItem.value) {
  392. videoItem.value.playbackRate(
  393. videoFroms.defaultSpeed
  394. );
  395. }
  396. }}>
  397. {{
  398. thumb: () => (
  399. <div class={styles.sliderPoint}>
  400. {videoFroms.defaultSpeed}
  401. <span>x</span>
  402. </div>
  403. )
  404. }}
  405. </NSlider>
  406. <i
  407. class={[styles.iconCut]}
  408. onClick={() => {
  409. if (videoFroms.defaultSpeed <= 0.5) {
  410. return;
  411. }
  412. if (videoItem.value) {
  413. videoFroms.defaultSpeed =
  414. (videoFroms.defaultSpeed * 10 - 1) / 10;
  415. videoItem.value.playbackRate(
  416. videoFroms.defaultSpeed
  417. );
  418. }
  419. }}></i>
  420. </div>
  421. </div>
  422. </div>
  423. <button class={styles.iconReplay} onClick={onReplay}>
  424. <img src={iconLoop} />
  425. </button>
  426. <div
  427. class={styles.actionBtn}
  428. onClick={() => {
  429. videoFroms.speedControl = false;
  430. onToggleVideo();
  431. }}>
  432. {videoFroms.paused ? (
  433. <img class={styles.playIcon} src={iconplay} />
  434. ) : (
  435. <img class={styles.playIcon} src={iconpause} />
  436. )}
  437. </div>
  438. <div class={styles.downloadSpeed}>
  439. {videoFroms.speedInKbps}
  440. </div>
  441. </div>
  442. </div>
  443. </>
  444. ) : (
  445. <>
  446. <div class={styles.actions}>
  447. <div class={styles.actionWrap}>
  448. <div
  449. class={styles.actionBtn}
  450. onClick={() => {
  451. videoFroms.speedControl = false;
  452. onToggleVideo();
  453. }}>
  454. {videoFroms.paused ? (
  455. <img class={styles.playIcon} src={iconplay} />
  456. ) : (
  457. <img class={styles.playIcon} src={iconpause} />
  458. )}
  459. </div>
  460. <button class={styles.iconReplay} onClick={onReplay}>
  461. <img src={iconLoop} />
  462. </button>
  463. <div
  464. class={styles.actionBtnSpeed}
  465. onClick={e => {
  466. e.stopPropagation();
  467. videoFroms.speedControl = !videoFroms.speedControl;
  468. }}>
  469. <img src={iconSpeed} />
  470. <div
  471. style={{
  472. display: videoFroms.speedControl ? 'block' : 'none'
  473. }}>
  474. <div
  475. class={styles.sliderPopup}
  476. onClick={(e: Event) => {
  477. e.stopPropagation();
  478. }}>
  479. <i
  480. class={styles.iconAdd}
  481. onClick={() => {
  482. if (videoFroms.defaultSpeed >= 1.5) {
  483. return;
  484. }
  485. if (videoItem.value) {
  486. videoFroms.defaultSpeed =
  487. (videoFroms.defaultSpeed * 10 + 1) / 10;
  488. videoItem.value.playbackRate(
  489. videoFroms.defaultSpeed
  490. );
  491. }
  492. }}></i>
  493. <NSlider
  494. value={videoFroms.defaultSpeed}
  495. step={0.1}
  496. max={1.5}
  497. min={0.5}
  498. vertical
  499. tooltip={false}
  500. onUpdate:value={(val: number) => {
  501. videoFroms.defaultSpeed = val;
  502. if (videoItem.value) {
  503. videoItem.value.playbackRate(
  504. videoFroms.defaultSpeed
  505. );
  506. }
  507. }}>
  508. {{
  509. thumb: () => (
  510. <div class={styles.sliderPoint}>
  511. {videoFroms.defaultSpeed}
  512. <span>x</span>
  513. </div>
  514. )
  515. }}
  516. </NSlider>
  517. <i
  518. class={[styles.iconCut]}
  519. onClick={() => {
  520. if (videoFroms.defaultSpeed <= 0.5) {
  521. return;
  522. }
  523. if (videoItem.value) {
  524. videoFroms.defaultSpeed =
  525. (videoFroms.defaultSpeed * 10 - 1) / 10;
  526. videoItem.value.playbackRate(
  527. videoFroms.defaultSpeed
  528. );
  529. }
  530. }}></i>
  531. </div>
  532. </div>
  533. </div>
  534. <div class={styles.downloadSpeed}>
  535. {videoFroms.speedInKbps}
  536. </div>
  537. </div>
  538. </div>
  539. <div class={styles.actions}>
  540. <div class={styles.actionWrap}>
  541. <div class={styles.time}>
  542. <div
  543. class="plyr__time plyr__time--current"
  544. aria-label="Current time">
  545. {videoFroms.currentTime}
  546. </div>
  547. <span class={styles.line}>/</span>
  548. <div
  549. class="plyr__time plyr__time--duration"
  550. aria-label="Duration">
  551. {videoFroms.duration}
  552. </div>
  553. </div>
  554. </div>
  555. </div>
  556. </>
  557. )}
  558. </div>
  559. </div>
  560. </div>
  561. );
  562. }
  563. });