audio-pay.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690
  1. import {
  2. defineComponent,
  3. reactive,
  4. ref,
  5. nextTick,
  6. onMounted,
  7. watch,
  8. onUnmounted
  9. } from 'vue';
  10. import styles from './audio.module.less';
  11. import iconplay from '../image/icon-pause.png';
  12. import iconpause from '../image/icon-play.png';
  13. import iconReplay from '../image/icon-replay.png';
  14. import { NSlider, useMessage } from 'naive-ui';
  15. import Vudio from 'vudio.js';
  16. import tickMp3 from '../image/tick.mp3';
  17. export default defineComponent({
  18. name: 'audio-play',
  19. props: {
  20. item: {
  21. type: Object,
  22. default: () => {
  23. return {};
  24. }
  25. },
  26. activeStatus: {
  27. type: Boolean,
  28. default: false
  29. },
  30. isEmtry: {
  31. type: Boolean,
  32. default: false
  33. },
  34. showModel: {
  35. type: Boolean,
  36. default: false
  37. },
  38. imagePos: {
  39. type: String,
  40. default: 'left'
  41. }
  42. },
  43. emits: ['loadedmetadata', 'togglePlay', 'ended', 'reset'],
  44. setup(props, { emit, expose }) {
  45. const message = useMessage()
  46. const audioForms = reactive({
  47. paused: true,
  48. speedInKbps: '',
  49. currentTimeNum: 0,
  50. isOnline: false,
  51. currentTime: '00:00',
  52. durationNum: 0,
  53. duration: '00:00',
  54. showBar: true,
  55. showAction: true,
  56. afterMa3: true,
  57. count: 0,
  58. previousBytesLoaded: 0,
  59. previousTime: Date.now(),
  60. // 添加缓冲状态标识
  61. isBuffering: false,
  62. bufferTimeout: null as any
  63. });
  64. const canvas: any = ref();
  65. const audio: any = ref();
  66. let vudio: any = null;
  67. // const audioSlider =
  68. // 'audioSlider' + Date.now() + Math.floor(Math.random() * 100);
  69. // 切换音频播放
  70. const onToggleAudio = (e?: any) => {
  71. e?.stopPropagation();
  72. // console.log(audio.value.paused, 'audio.value.paused');
  73. if (audio.value.paused) {
  74. audio.value.play();
  75. audioForms.afterMa3 = false;
  76. } else {
  77. audio.value?.pause();
  78. }
  79. audioForms.paused = audio.value?.paused;
  80. e?.target?.focus();
  81. emit('togglePlay', audioForms.paused);
  82. };
  83. const onInit = (audio: undefined, canvas: undefined) => {
  84. if (!vudio) {
  85. vudio = new Vudio(audio, canvas, {
  86. effect: 'waveform',
  87. accuracy: 256,
  88. width: 1024,
  89. height: 600,
  90. waveform: {
  91. maxHeight: 200,
  92. color: [
  93. [0, '#44D1FF'],
  94. [0.5, '#44D1FF'],
  95. [0.5, '#198CFE'],
  96. [1, '#198CFE']
  97. ],
  98. prettify: false
  99. }
  100. });
  101. vudio.dance();
  102. }
  103. };
  104. // 对时间进行格式化
  105. const timeFormat = (num: number) => {
  106. if (num > 0) {
  107. const m = Math.floor(num / 60);
  108. const s = num % 60;
  109. return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
  110. } else {
  111. return '00:00';
  112. }
  113. };
  114. //
  115. const toggleHideControl = (isShow: false) => {
  116. audioForms.showBar = isShow;
  117. };
  118. const onReplay = () => {
  119. if (!audio.value) return;
  120. audio.value.currentTime = 0;
  121. };
  122. let vudio1 = null;
  123. const canvas1: any = ref();
  124. const audio1: any = ref();
  125. nextTick(() => {
  126. vudio1 = new Vudio(audio1.value, canvas1.value, {
  127. effect: 'waveform',
  128. accuracy: 256,
  129. width: 1024,
  130. height: 600,
  131. waveform: {
  132. maxHeight: 200,
  133. color: [
  134. [0, '#44D1FF'],
  135. [0.5, '#44D1FF'],
  136. [0.5, '#198CFE'],
  137. [1, '#198CFE']
  138. ],
  139. prettify: false
  140. }
  141. });
  142. vudio1.dance();
  143. });
  144. watch(
  145. () => props.activeStatus,
  146. (val: any) => {
  147. // console.log(val, 'val');
  148. audioForms.count = 0;
  149. if (val && props.item.autoPlay) {
  150. vudio = null;
  151. onToggleAudio();
  152. } else {
  153. audio.value.pause();
  154. audioForms.paused = audio.value?.paused;
  155. }
  156. }
  157. );
  158. watch(
  159. () => props.showModel,
  160. () => {
  161. // console.log(props.showModel, 'props.showModel')
  162. audioForms.showAction = props.showModel;
  163. }
  164. );
  165. const calculateSpeed = (element: any) => {
  166. let previousBytesLoaded = 0;
  167. let timer: any = null;
  168. let previousTime = Date.now();
  169. let buffterCatchArray = [] as any; // 缓存数据显示
  170. let isWaiting = false;
  171. // 缓存检测状态
  172. let isBuffering = false;
  173. // 缓存检测计时器
  174. let bufferTimeout: any = null;
  175. // 设定一个检测缓存停止的时间间隔,这里我们设置为2500毫秒(2秒)
  176. const BUFFER_CHECK_INTERVAL = 2500;
  177. function resetDownloadSpeed() {
  178. timer = setTimeout(() => {
  179. // displayElement.textContent = `视屏下载速度: 0 KB/s`;
  180. audioForms.speedInKbps = `0 KB/s`;
  181. }, 1500);
  182. }
  183. function buffterCatch(time = 0) {
  184. // 设定一个计时器,检查是否在指定的时间内再次触发了progress事件
  185. bufferTimeout = setTimeout(() => {
  186. if (isBuffering) {
  187. // 如果计时器到达且isBuffering仍为true,则认为缓存停止
  188. console.log('停止缓存数据。');
  189. isBuffering = false;
  190. audioForms.speedInKbps = '';
  191. }
  192. }, time || BUFFER_CHECK_INTERVAL);
  193. }
  194. function resetBuffterCatch() {
  195. // 如果有缓存检测计时器,则清除它
  196. if (bufferTimeout) {
  197. clearTimeout(bufferTimeout);
  198. }
  199. // 标记为正在缓存
  200. isBuffering = true;
  201. buffterCatch();
  202. }
  203. /**
  204. * 格式化视屏播放有效时间 - 合并区间
  205. * @param intervals [[], []]
  206. * @example [[4, 8],[0, 4],[10, 30]]
  207. * @returns [[0, 8], [10, 30]]
  208. */
  209. const formatEffectiveTime = (intervals: any[]) => {
  210. const res: any = [];
  211. intervals.sort((a, b) => a[0] - b[0]);
  212. let prev = intervals[0];
  213. for (let i = 1; i < intervals.length; i++) {
  214. const cur = intervals[i];
  215. if (prev[1] >= cur[0]) {
  216. // 有重合
  217. prev[1] = Math.max(cur[1], prev[1]);
  218. } else {
  219. // 不重合,prev推入res数组
  220. res.push(prev);
  221. prev = cur; // 更新 prev
  222. }
  223. }
  224. res.push(prev);
  225. return res;
  226. };
  227. // 获取视频总时长
  228. let noData = false;
  229. const onProgress = () => {
  230. const duration = element.duration;
  231. const currentTime = Date.now();
  232. const buffered = element.buffered;
  233. let currentBytesLoaded = 0;
  234. const currentLength = 0;
  235. // 计算视频已缓存的总时长
  236. let cachedDuration = 0;
  237. if (buffered.length > 0) {
  238. for (let i = 0; i < buffered.length; i++) {
  239. currentBytesLoaded += buffered.end(i) - buffered.start(i);
  240. cachedDuration += buffered.end(i) - buffered.start(i);
  241. buffterCatchArray = formatEffectiveTime([
  242. ...buffterCatchArray,
  243. [buffered.start(i), buffered.end(i)]
  244. ]);
  245. }
  246. // for (let i = 0; i < buffered.length; i++) {
  247. // // 寻找当前时间之后最近的点
  248. // if (buffered.start(buffered.length - 1 - i) < element.currentTime) {
  249. // currentLength =
  250. // (buffered.end(buffered.length - 1 - i) / duration) * 100;
  251. // break;
  252. // }
  253. // }
  254. currentBytesLoaded *= element.duration * element.seekable.end(0); // 更精确地近似字节加载量
  255. }
  256. // 计算未缓存的时间段
  257. const uncachedDuration = duration - cachedDuration;
  258. let uncachedTime = true; // 没有缓存时间
  259. buffterCatchArray.forEach((item: any) => {
  260. if (element.currentTime >= item[0] && uncachedTime <= item[1]) {
  261. uncachedTime = false;
  262. }
  263. });
  264. // if (duration) {
  265. // const sliderDom: any = document.querySelector(
  266. // '#' + audioSlider + ' .n-slider'
  267. // );
  268. // if (sliderDom) {
  269. // sliderDom.style.setProperty(
  270. // '--catch-width',
  271. // uncachedDuration > 0 ? `${currentLength}%` : 'calc(100% + 17px)'
  272. // );
  273. // }
  274. // }
  275. // console.log(
  276. // uncachedTime,
  277. // duration,
  278. // cachedDuration,
  279. // 'duration',
  280. // buffterCatchArray,
  281. // element.currentTime,
  282. // currentLength + '%',
  283. // isWaiting,
  284. // currentBytesLoaded <= previousBytesLoaded
  285. // );
  286. const isNoBuffer = currentBytesLoaded <= previousBytesLoaded;
  287. // console.log(
  288. // 'progress',
  289. // currentBytesLoaded,
  290. // previousBytesLoaded,
  291. // currentBytesLoaded > previousBytesLoaded
  292. // );
  293. // 计算未缓存的时间段
  294. // const uncachedDuration = duration - cachedDuration;
  295. // console.log(uncachedDuration, duration, cachedDuration, 'duration');
  296. // 如果存在未缓存的时间段,可以根据具体情况做出相应处理
  297. if (uncachedDuration > 0) {
  298. if (currentBytesLoaded > previousBytesLoaded && audioForms.isOnline) {
  299. const timeDiff = (currentTime - previousTime) / 1000; // 时间差转换为秒
  300. const bytesDiff = currentBytesLoaded - previousBytesLoaded; // 字节差值
  301. const speed = bytesDiff / timeDiff; // 字节每秒
  302. if (!element.paused) {
  303. const kbps = speed / 1024;
  304. const speedInKbps = kbps.toFixed(2); // 转换为千字节每秒并保留两位小数
  305. if (kbps > 1024) {
  306. audioForms.speedInKbps = `${Number(
  307. (kbps / 1024).toFixed(2)
  308. )} M/s`;
  309. } else {
  310. audioForms.speedInKbps = `${Number(speedInKbps)} KB/s`;
  311. }
  312. // 如果1秒钟没有返回就重置数据
  313. clearTimeout(timer);
  314. resetDownloadSpeed();
  315. }
  316. previousBytesLoaded = currentBytesLoaded;
  317. previousTime = currentTime;
  318. noData = false;
  319. }
  320. if (isNoBuffer && !uncachedTime) {
  321. // 如果1秒钟没有返回就重置数据
  322. if (!noData) {
  323. clearTimeout(timer);
  324. clearTimeout(bufferTimeout);
  325. setTimeout(() => {
  326. if (isBuffering) {
  327. // 如果计时器到达且isBuffering仍为true,则认为缓存停止
  328. console.log('停止缓存数据');
  329. isBuffering = false;
  330. audioForms.speedInKbps = '';
  331. }
  332. }, 800);
  333. }
  334. noData = true;
  335. }
  336. if (element.paused || !audioForms.isOnline) {
  337. clearTimeout(timer);
  338. audioForms.speedInKbps = '';
  339. }
  340. if (!isWaiting) {
  341. resetBuffterCatch();
  342. }
  343. } else {
  344. clearTimeout(timer);
  345. buffterCatch(1000);
  346. }
  347. };
  348. const onWaiting = () => {
  349. console.log('waiting');
  350. isWaiting = true;
  351. let uncachedTime = true; // 没有缓存时间
  352. buffterCatchArray.forEach((item: any) => {
  353. if (element.currentTime >= item[0] && uncachedTime <= item[1]) {
  354. uncachedTime = false;
  355. }
  356. });
  357. if (!element.paused && audioForms.isOnline && uncachedTime) {
  358. // 如果1秒钟没有返回就重置数据
  359. clearTimeout(timer);
  360. resetDownloadSpeed();
  361. }
  362. // 如果有缓存检测计时器,则清除它
  363. if (bufferTimeout) {
  364. clearTimeout(bufferTimeout);
  365. }
  366. };
  367. const onCanplay = () => {
  368. console.log('canplay');
  369. isWaiting = false;
  370. resetBuffterCatch();
  371. };
  372. const onPause = () => {
  373. clearTimeout(timer);
  374. // 如果有缓存检测计时器,则清除它
  375. if (bufferTimeout) {
  376. clearTimeout(bufferTimeout);
  377. }
  378. audioForms.speedInKbps = '';
  379. };
  380. element.removeEventListener('progress', onProgress);
  381. element.removeEventListener('waiting', onWaiting);
  382. element.removeEventListener('canplay', onCanplay);
  383. element.removeEventListener('pause', onPause);
  384. element.addEventListener('progress', onProgress);
  385. element.addEventListener('waiting', onWaiting);
  386. element.addEventListener('canplay', onCanplay);
  387. element.addEventListener('pause', onPause);
  388. };
  389. const onChangeOnlineStatus = (val: any) => {
  390. if (val.type === 'online') {
  391. audioForms.isOnline = true;
  392. const currentTime = audio.value.currentTime;
  393. audio.value.load();
  394. audio.value.currentTime = currentTime;
  395. audio.value.pause();
  396. audioForms.paused = audio.value?.paused;
  397. } else if (val.type === 'offline') {
  398. audioForms.isOnline = false;
  399. console.log('offline');
  400. // 去掉检测加载缓存
  401. audioForms.isBuffering = false;
  402. audioForms.bufferTimeout && clearTimeout(audioForms.bufferTimeout);
  403. }
  404. };
  405. onMounted(() => {
  406. nextTick(() => {
  407. calculateSpeed(audio.value);
  408. });
  409. window.addEventListener('online', onChangeOnlineStatus);
  410. window.addEventListener('offline', onChangeOnlineStatus);
  411. });
  412. onUnmounted(() => {
  413. window.removeEventListener('online', onChangeOnlineStatus);
  414. window.removeEventListener('offline', onChangeOnlineStatus);
  415. });
  416. expose({
  417. toggleHideControl,
  418. onToggleAudio
  419. });
  420. return () => (
  421. <div class={styles.audioWrap}>
  422. <div class={styles.audioContainer}>
  423. <div class={styles.audioTitle}>{props.item.title}</div>
  424. <audio
  425. ref={audio}
  426. crossorigin="anonymous"
  427. src={props.item.content}
  428. onEnded={() => {
  429. audioForms.paused = true;
  430. emit('ended');
  431. }}
  432. onPlay={() => {
  433. audioForms.paused = audio.value?.paused;
  434. }}
  435. onPause={() => {
  436. // audioForms.paused = audio.value?.paused;
  437. }}
  438. onTimeupdate={() => {
  439. audioForms.currentTime = timeFormat(
  440. Math.round(audio.value?.currentTime || 0)
  441. );
  442. audioForms.currentTimeNum = audio.value?.currentTime || 0;
  443. if (audioForms.count <= 1) {
  444. audioForms.count += 1;
  445. onInit(audio.value, canvas.value);
  446. }
  447. }}
  448. onLoadedmetadata={() => {
  449. audioForms.duration = timeFormat(
  450. Math.round(audio.value?.duration)
  451. );
  452. audioForms.durationNum = audio.value?.duration;
  453. if (props.item.autoPlay && audio.value && props.activeStatus) {
  454. // audio.value.play();
  455. onToggleAudio();
  456. }
  457. if (audio.value) {
  458. audio.value.stop = () => {
  459. audio.value?.pause();
  460. audioForms.paused = true;
  461. emit('togglePlay', audioForms.paused);
  462. };
  463. audio.value.onPlay = () => {
  464. audio.value?.play();
  465. audioForms.paused = false;
  466. onInit(audio.value, canvas.value);
  467. emit('togglePlay', audioForms.paused);
  468. };
  469. }
  470. emit('loadedmetadata', audio.value);
  471. }}
  472. onWaiting={() => {
  473. audioForms.isBuffering = true;
  474. audioForms.bufferTimeout &&
  475. clearTimeout(audioForms.bufferTimeout);
  476. // 设置缓冲超时检测(15秒)
  477. audioForms.bufferTimeout = setTimeout(() => {
  478. if (audioForms.isBuffering && audioForms.isOnline) {
  479. console.log('缓冲超时,暂停播放');
  480. audio.value?.pause();
  481. audioForms.paused = true;
  482. message.info('网络连接不稳定,请检查网络后继续播放', {
  483. showIcon: false,
  484. render: (props: any) => {
  485. return (
  486. <div style="background: rgba(0,0,0,.6); color: #fff; border-radius: 8px; padding: 6px 16px;">
  487. {props.content}
  488. </div>
  489. );
  490. }
  491. });
  492. audio.value.currentTime = audio.value.currentTime + 0.01;
  493. }
  494. }, 15000);
  495. }}
  496. onCanplay={() => {
  497. audioForms.isBuffering = false;
  498. audioForms.bufferTimeout && clearTimeout(audioForms.bufferTimeout); // 清除超时检测
  499. }}
  500. onError={() => {
  501. audio.value?.pause();
  502. audioForms.paused = audio.value?.paused;
  503. }}></audio>
  504. <canvas ref={canvas}></canvas>
  505. {audioForms.afterMa3 && (
  506. <div class={styles.tempVudio}>
  507. <audio ref={audio1} src={tickMp3} />
  508. <canvas ref={canvas1}></canvas>
  509. </div>
  510. )}
  511. </div>
  512. <div
  513. class={[
  514. styles.controls,
  515. audioForms.showAction ? '' : styles.sectionAnimate
  516. ]}
  517. onClick={(e: MouseEvent) => {
  518. e.stopPropagation();
  519. emit('reset');
  520. }}>
  521. <div class={styles.slider}>
  522. {props.imagePos !== 'right' && (
  523. <div class={styles.time}>
  524. <div
  525. class="plyr__time plyr__time--current"
  526. aria-label="Current time">
  527. {audioForms.currentTime}
  528. </div>
  529. <span class={styles.line}>/</span>
  530. <div
  531. class="plyr__time plyr__time--duration"
  532. aria-label="Duration">
  533. {audioForms.duration}
  534. </div>
  535. </div>
  536. )}
  537. <NSlider
  538. value={audioForms.currentTimeNum}
  539. step={0.01}
  540. max={audioForms.durationNum}
  541. tooltip={false}
  542. onUpdate:value={(val: number) => {
  543. audio.value.currentTime = val;
  544. audioForms.currentTimeNum = val;
  545. audioForms.currentTime = timeFormat(Math.round(val || 0));
  546. }}
  547. />
  548. {props.imagePos === 'right' && (
  549. <div class={[styles.time, styles.rightTime]}>
  550. <div
  551. class="plyr__time plyr__time--current"
  552. aria-label="Current time">
  553. {audioForms.currentTime}
  554. </div>
  555. <span class={styles.line}>/</span>
  556. <div
  557. class="plyr__time plyr__time--duration"
  558. aria-label="Duration">
  559. {audioForms.duration}
  560. </div>
  561. </div>
  562. )}
  563. </div>
  564. <div class={styles.tools}>
  565. {props.imagePos === 'right' ? (
  566. <>
  567. <div class={styles.actions}>
  568. <div class={styles.title}>{props.item.title}</div>
  569. {/* <div class={styles.time}>
  570. <div
  571. class="plyr__time plyr__time--current"
  572. aria-label="Current time">
  573. {audioForms.currentTime}
  574. </div>
  575. <span class={styles.line}>/</span>
  576. <div
  577. class="plyr__time plyr__time--duration"
  578. aria-label="Duration">
  579. {audioForms.duration}
  580. </div>
  581. </div> */}
  582. </div>
  583. <div class={styles.actions}>
  584. <div class={styles.actionWrap}>
  585. <div class={styles.downloadSpeed}>
  586. {audioForms.speedInKbps}
  587. </div>
  588. <button class={styles.iconReplay} onClick={onReplay}>
  589. <img src={iconReplay} />
  590. </button>
  591. <div class={styles.actionBtn} onClick={onToggleAudio}>
  592. {audioForms.paused ? (
  593. <img class={styles.playIcon} src={iconplay} />
  594. ) : (
  595. <img class={styles.playIcon} src={iconpause} />
  596. )}
  597. </div>
  598. </div>
  599. </div>
  600. </>
  601. ) : (
  602. <>
  603. <div class={styles.actions}>
  604. <div class={styles.actionWrap}>
  605. <div class={styles.actionBtn} onClick={onToggleAudio}>
  606. {audioForms.paused ? (
  607. <img class={styles.playIcon} src={iconplay} />
  608. ) : (
  609. <img class={styles.playIcon} src={iconpause} />
  610. )}
  611. </div>
  612. <button class={styles.iconReplay} onClick={onReplay}>
  613. <img src={iconReplay} />
  614. </button>
  615. <div class={styles.downloadSpeed}>
  616. {audioForms.speedInKbps}
  617. </div>
  618. </div>
  619. </div>
  620. <div class={styles.actions}>
  621. <div class={styles.title}>{props.item.title}</div>
  622. {/* <div class={styles.time}>
  623. <div
  624. class="plyr__time plyr__time--current"
  625. aria-label="Current time">
  626. {audioForms.currentTime}
  627. </div>
  628. <span class={styles.line}>/</span>
  629. <div
  630. class="plyr__time plyr__time--duration"
  631. aria-label="Duration">
  632. {audioForms.duration}
  633. </div>
  634. </div> */}
  635. </div>
  636. </>
  637. )}
  638. </div>
  639. </div>
  640. </div>
  641. );
  642. }
  643. });