audio-pay.tsx 19 KB

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