audio-pay.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  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. // 切换音频播放
  59. const onToggleAudio = (e?: any) => {
  60. e?.stopPropagation();
  61. // console.log(audio.value.paused, 'audio.value.paused');
  62. if (audio.value.paused) {
  63. audio.value.play();
  64. audioForms.afterMa3 = false;
  65. } else {
  66. audio.value?.pause();
  67. }
  68. audioForms.paused = audio.value?.paused;
  69. e?.target?.focus();
  70. emit('togglePlay', audioForms.paused);
  71. };
  72. const onInit = (audio: undefined, canvas: undefined) => {
  73. if (!vudio) {
  74. vudio = new Vudio(audio, canvas, {
  75. effect: 'waveform',
  76. accuracy: 256,
  77. width: 1024,
  78. height: 600,
  79. waveform: {
  80. maxHeight: 200,
  81. color: [
  82. [0, '#44D1FF'],
  83. [0.5, '#44D1FF'],
  84. [0.5, '#198CFE'],
  85. [1, '#198CFE']
  86. ],
  87. prettify: false
  88. }
  89. });
  90. vudio.dance();
  91. }
  92. };
  93. // 对时间进行格式化
  94. const timeFormat = (num: number) => {
  95. if (num > 0) {
  96. const m = Math.floor(num / 60);
  97. const s = num % 60;
  98. return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
  99. } else {
  100. return '00:00';
  101. }
  102. };
  103. //
  104. const toggleHideControl = (isShow: false) => {
  105. audioForms.showBar = isShow;
  106. };
  107. const onReplay = () => {
  108. if (!audio.value) return;
  109. audio.value.currentTime = 0;
  110. };
  111. let vudio1 = null;
  112. const canvas1: any = ref();
  113. const audio1: any = ref();
  114. nextTick(() => {
  115. vudio1 = new Vudio(audio1.value, canvas1.value, {
  116. effect: 'waveform',
  117. accuracy: 256,
  118. width: 1024,
  119. height: 600,
  120. waveform: {
  121. maxHeight: 200,
  122. color: [
  123. [0, '#44D1FF'],
  124. [0.5, '#44D1FF'],
  125. [0.5, '#198CFE'],
  126. [1, '#198CFE']
  127. ],
  128. prettify: false
  129. }
  130. });
  131. vudio1.dance();
  132. });
  133. watch(
  134. () => props.activeStatus,
  135. (val: any) => {
  136. // console.log(val, 'val');
  137. audioForms.count = 0;
  138. if (val && props.item.autoPlay) {
  139. vudio = null;
  140. onToggleAudio();
  141. } else {
  142. audio.value.pause();
  143. }
  144. }
  145. );
  146. const calculateSpeed = (element: any) => {
  147. let previousBytesLoaded = 0;
  148. let timer: any = null;
  149. let previousTime = Date.now();
  150. let isWaiting = false;
  151. // 缓存检测状态
  152. let isBuffering = false;
  153. // 缓存检测计时器
  154. let bufferTimeout: any = null;
  155. // 设定一个检测缓存停止的时间间隔,这里我们设置为2500毫秒(2秒)
  156. const BUFFER_CHECK_INTERVAL = 2500;
  157. function resetDownloadSpeed() {
  158. timer = setTimeout(() => {
  159. // displayElement.textContent = `视屏下载速度: 0 KB/s`;
  160. audioForms.speedInKbps = `0 KB/s`;
  161. }, 1500);
  162. }
  163. function buffterCatch() {
  164. // 设定一个计时器,检查是否在指定的时间内再次触发了progress事件
  165. bufferTimeout = setTimeout(() => {
  166. if (isBuffering) {
  167. // 如果计时器到达且isBuffering仍为true,则认为缓存停止
  168. console.log('停止缓存数据。');
  169. isBuffering = false;
  170. audioForms.speedInKbps = '';
  171. }
  172. }, BUFFER_CHECK_INTERVAL);
  173. }
  174. function resetBuffterCatch() {
  175. // 如果有缓存检测计时器,则清除它
  176. if (bufferTimeout) {
  177. clearTimeout(bufferTimeout);
  178. }
  179. // 标记为正在缓存
  180. isBuffering = true;
  181. buffterCatch();
  182. }
  183. element.addEventListener('loadedmetadata', () => {
  184. // 获取视频总时长
  185. const duration = element.duration;
  186. element.addEventListener('progress', () => {
  187. const currentTime = Date.now();
  188. const buffered = element.buffered;
  189. let currentBytesLoaded = 0;
  190. // 计算视频已缓存的总时长
  191. let cachedDuration = 0;
  192. if (buffered.length > 0) {
  193. for (let i = 0; i < buffered.length; i++) {
  194. currentBytesLoaded += buffered.end(i) - buffered.start(i);
  195. cachedDuration += buffered.end(i) - buffered.start(i);
  196. }
  197. currentBytesLoaded *= element.duration * element.seekable.end(0); // 更精确地近似字节加载量
  198. }
  199. // console.log(
  200. // 'progress',
  201. // currentBytesLoaded,
  202. // previousBytesLoaded,
  203. // currentBytesLoaded > previousBytesLoaded
  204. // );
  205. // 计算未缓存的时间段
  206. const uncachedDuration = duration - cachedDuration;
  207. console.log(uncachedDuration, duration, cachedDuration, 'duration');
  208. // 如果存在未缓存的时间段,可以根据具体情况做出相应处理
  209. if (uncachedDuration > 0) {
  210. if (
  211. currentBytesLoaded > previousBytesLoaded &&
  212. audioForms.isOnline
  213. ) {
  214. const timeDiff = (currentTime - previousTime) / 1000; // 时间差转换为秒
  215. const bytesDiff = currentBytesLoaded - previousBytesLoaded; // 字节差值
  216. const speed = bytesDiff / timeDiff; // 字节每秒
  217. if (!element.paused) {
  218. const kbps = speed / 1024;
  219. const speedInKbps = kbps.toFixed(2); // 转换为千字节每秒并保留两位小数
  220. if (kbps > 1024) {
  221. audioForms.speedInKbps = `${Number(
  222. (kbps / 1024).toFixed(2)
  223. )} M/s`;
  224. } else {
  225. audioForms.speedInKbps = `${Number(speedInKbps)} KB/s`;
  226. }
  227. }
  228. previousBytesLoaded = currentBytesLoaded;
  229. previousTime = currentTime;
  230. }
  231. if (!element.paused && audioForms.isOnline) {
  232. // 如果1秒钟没有返回就重置数据
  233. clearTimeout(timer);
  234. resetDownloadSpeed();
  235. } else {
  236. audioForms.speedInKbps = '';
  237. }
  238. resetBuffterCatch();
  239. } else {
  240. resetBuffterCatch();
  241. }
  242. });
  243. element.addEventListener('waiting', () => {
  244. console.log('waiting');
  245. isWaiting = true;
  246. if (!element.paused) {
  247. // 如果1秒钟没有返回就重置数据
  248. clearTimeout(timer);
  249. resetDownloadSpeed();
  250. }
  251. // 如果有缓存检测计时器,则清除它
  252. if (bufferTimeout) {
  253. clearTimeout(bufferTimeout);
  254. }
  255. });
  256. element.addEventListener('canplay', () => {
  257. console.log('canplay');
  258. isWaiting = false;
  259. resetBuffterCatch();
  260. });
  261. element.addEventListener('pause', () => {
  262. clearTimeout(timer);
  263. // 如果有缓存检测计时器,则清除它
  264. if (bufferTimeout) {
  265. clearTimeout(bufferTimeout);
  266. }
  267. audioForms.speedInKbps = '';
  268. });
  269. // element.addEventListener('error', () => {
  270. // element.pause();
  271. // });
  272. });
  273. };
  274. const onChangeOnlineStatus = (val: any) => {
  275. if (val.type === 'online') {
  276. audioForms.isOnline = true;
  277. } else if (val.type === 'offline') {
  278. audioForms.isOnline = false;
  279. }
  280. };
  281. onMounted(() => {
  282. nextTick(() => {
  283. calculateSpeed(audio.value);
  284. });
  285. window.addEventListener('online', onChangeOnlineStatus);
  286. window.addEventListener('offline', onChangeOnlineStatus);
  287. });
  288. onUnmounted(() => {
  289. window.removeEventListener('online', onChangeOnlineStatus);
  290. window.removeEventListener('offline', onChangeOnlineStatus);
  291. });
  292. expose({
  293. toggleHideControl
  294. });
  295. return () => (
  296. <div class={styles.audioWrap}>
  297. <div class={styles.audioContainer}>
  298. <audio
  299. ref={audio}
  300. crossorigin="anonymous"
  301. src={props.item.content}
  302. onEnded={() => {
  303. audioForms.paused = true;
  304. emit('ended');
  305. }}
  306. onTimeupdate={() => {
  307. audioForms.currentTime = timeFormat(
  308. Math.round(audio.value?.currentTime || 0)
  309. );
  310. audioForms.currentTimeNum = audio.value?.currentTime || 0;
  311. if (audioForms.count <= 1) {
  312. audioForms.count += 1;
  313. onInit(audio.value, canvas.value);
  314. }
  315. }}
  316. onLoadedmetadata={() => {
  317. audioForms.duration = timeFormat(
  318. Math.round(audio.value?.duration)
  319. );
  320. audioForms.durationNum = audio.value?.duration;
  321. if (props.item.autoPlay && audio.value && props.activeStatus) {
  322. // audio.value.play();
  323. onToggleAudio();
  324. }
  325. if (audio.value) {
  326. audio.value.stop = () => {
  327. audio.value?.pause();
  328. audioForms.paused = true;
  329. emit('togglePlay', audioForms.paused);
  330. };
  331. audio.value.onPlay = () => {
  332. audio.value?.play();
  333. audioForms.paused = false;
  334. onInit(audio.value, canvas.value);
  335. emit('togglePlay', audioForms.paused);
  336. };
  337. }
  338. emit('loadedmetadata', audio.value);
  339. }}
  340. onProgress={(e: any) => {
  341. console.log(e, 'loadedmetadata onProgress');
  342. }}></audio>
  343. <canvas ref={canvas}></canvas>
  344. {audioForms.afterMa3 && (
  345. <div class={styles.tempVudio}>
  346. <audio ref={audio1} src={tickMp3} />
  347. <canvas ref={canvas1}></canvas>
  348. </div>
  349. )}
  350. </div>
  351. <div
  352. class={[
  353. styles.controls,
  354. audioForms.showBar ? '' : styles.sectionAnimate
  355. ]}
  356. onClick={(e: MouseEvent) => {
  357. e.stopPropagation();
  358. emit('reset');
  359. }}>
  360. <div class={styles.slider}>
  361. <NSlider
  362. value={audioForms.currentTimeNum}
  363. step={0.01}
  364. max={audioForms.durationNum}
  365. tooltip={false}
  366. onUpdate:value={(val: number) => {
  367. audio.value.currentTime = val;
  368. audioForms.currentTimeNum = val;
  369. audioForms.currentTime = timeFormat(Math.round(val || 0));
  370. }}
  371. />
  372. </div>
  373. <div class={styles.tools}>
  374. {props.imagePos === 'right' ? (
  375. <>
  376. <div class={styles.actions}>
  377. <div class={styles.time}>
  378. <div
  379. class="plyr__time plyr__time--current"
  380. aria-label="Current time">
  381. {audioForms.currentTime}
  382. </div>
  383. <span class={styles.line}>/</span>
  384. <div
  385. class="plyr__time plyr__time--duration"
  386. aria-label="Duration">
  387. {audioForms.duration}
  388. </div>
  389. </div>
  390. </div>
  391. <div class={styles.actions}>
  392. <div class={styles.actionWrap}>
  393. <div class={styles.downloadSpeed}>
  394. {audioForms.speedInKbps}
  395. </div>
  396. <button class={styles.iconReplay} onClick={onReplay}>
  397. <img src={iconReplay} />
  398. </button>
  399. <div class={styles.actionBtn} onClick={onToggleAudio}>
  400. {audioForms.paused ? (
  401. <img class={styles.playIcon} src={iconplay} />
  402. ) : (
  403. <img class={styles.playIcon} src={iconpause} />
  404. )}
  405. </div>
  406. </div>
  407. </div>
  408. </>
  409. ) : (
  410. <>
  411. <div class={styles.actions}>
  412. <div class={styles.actionWrap}>
  413. <div class={styles.actionBtn} onClick={onToggleAudio}>
  414. {audioForms.paused ? (
  415. <img class={styles.playIcon} src={iconplay} />
  416. ) : (
  417. <img class={styles.playIcon} src={iconpause} />
  418. )}
  419. </div>
  420. <button class={styles.iconReplay} onClick={onReplay}>
  421. <img src={iconReplay} />
  422. </button>
  423. <div class={styles.downloadSpeed}>
  424. {audioForms.speedInKbps}
  425. </div>
  426. </div>
  427. </div>
  428. <div class={styles.actions}>
  429. <div class={styles.time}>
  430. <div
  431. class="plyr__time plyr__time--current"
  432. aria-label="Current time">
  433. {audioForms.currentTime}
  434. </div>
  435. <span class={styles.line}>/</span>
  436. <div
  437. class="plyr__time plyr__time--duration"
  438. aria-label="Duration">
  439. {audioForms.duration}
  440. </div>
  441. </div>
  442. </div>
  443. </>
  444. )}
  445. </div>
  446. </div>
  447. </div>
  448. );
  449. }
  450. });