做即时通讯,除了文字、图片、表情、还有媒体消息,整理一下制作过程中自定义聊天框中的audio
效果图
tsx完整代码
AzEventBus 是解决点击多个语音播放时候,保证只有一个在播放;没什么特别的,就是自己简单封装了个EvenBusAzEventBus
import {useRef, useEffect, useState, memo} from 'react';
import IconfontSvg from '@/views/Components/IconfontSvg';
interface PropsType {
// 播放地址
url: string,
// 允许父组件把class传入进来
className?: string,
// 宽度
width?: number,
// 高度
height?: number,
// 是否是自己发送的消息,就像微信/钉钉;右侧是自己发消息左侧是别人发布消息;
isSelfSend: boolean,
// 消息唯一id, 用来比对当前全局在播放的是不是自己,不是自己就暂停自己
mid: string
}
import AzEventBus from '@/views/Utils/EventBus';
import style from './lazyAudio.module.less';
// preload="metadata"
// 辅助函数:格式化时间为'MM:SS'或'HH:MM:SS'
const formatTime = (time: number) => {
const hours = Math.floor(time / 3600);
const minutes = Math.floor((time % 3600) / 60);
const seconds = Math.floor(time % 60);
// if (hours > 0) {
// return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
// } else {
// return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
// }
if (hours > 0) {
return `${String(hours)}时${String(minutes)}分${String(seconds)}秒`;
} else {
// return minutes > 0 ? `${String(minutes)}分${String(seconds)}秒` : `${String(time.toFixed(2))}秒`;
if (minutes > 0) {
return `${String(minutes)}分${String(seconds)}秒`;
}
// 如果是整数秒时
if (Number.isInteger(time)) {
return time;
}
// 如果是小数,检查小数点后的位数
const decimalPart = time.toString().split('.')[1];
const decimalLength = decimalPart ? decimalPart.length : 0;
/* 如果小数点后超过两位,则四舍五入到两位小数
如果小数点后不足两位,则直接转换为字符串,不补零
*/
return decimalLength > 2 ? `${time.toFixed(2)}秒` : `${time.toString()}秒`;
}
};
const LazyAudio = ({url, className= '', width = 300, height = 54, isSelfSend, mid}: PropsType) => {
const currentRef: any = useRef(null);
const audioRef = useRef<any>(null);
const [isPlaying, setIsPlaying] = useState(false);
const [duration, setDuration] = useState('0');
useEffect(() => {
/* <audio>
src: 路径
autoplay: 自动播放
autobuffer: 自动缓存
loop: 循环播放
currentTime: 播放的时间
muted: 静音
preload: 预加载 'auto'_指示一旦页面加载,则开始加载音频 | 'metadata'_当页面加载后仅加载音频的元数据 | 'none'_页面加载后不应加载音频
*/
/*
只读属性
只读属性 currentTime 表示当前播放的时间(以秒为单位)。
只读属性 duration 表示音频的总时长(以秒为单位)。
只读属性 paused 表示音频是否正在暂停。
只读属性 ended 表示音频是否已经播放完毕。
只读属性 palyed 表示音频是否已经播放。
只读属性 loop 表示音频是否循环播放。
只读属性 error 表示音频的错误信息。
只读属性 currentSrc 表示当前正在播放的音频源。
只读属性 defaultPlaybackRate 表示音频的默认播放速度。
只读属性 playbackRate 表示音频的当前播放速度。
只读属性 volume 表示音频的音量(0.0 到 1.0 之间)。
只读属性 networkState 表示音频的网络状态。
只读属性 readyState 表示音频的就绪状态。
*/
const audioElement: HTMLAudioElement = audioRef.current;
// 时长
const loadedmetadataEvent = (event: any) => {
const audio = event.target;
const audioDuration = audio.duration;
if (audioDuration === Infinity) {
audio.currentTime = 1e101;
audio.ontimeupdate = () => {
audio.ontimeupdate = () => { return; };
audio.currentTime = 1e101;
audio.currentTime = 0;
const duration = audio.duration;
// console.log('计算时长', formatTime(duration));
setDuration(duration.toFixed(2));
}
} else {
const duration = audio.duration;
setDuration(duration.toFixed(2));
// console.log('计算时长', formatTime(duration));
}
};
// 播放进度
const timeupdateEvent = () => {
// console.log('audioElement.currentTime', audioElement.currentTime, audioElement.duration);
// setCurrentTime(audioElement.currentTime);
};
// 播放完毕
const onAudioEndedEvent = () => {
// 在这里添加您的逻辑,例如播放下一个音频或执行其他操作
setIsPlaying(false);
};
// 播放错误
const playErrorEvent = () => {
// setHasError(true);
setIsPlaying(false);
};
// 播放_回调
const handlePlayEvent = () => {
// 可以在每次播放的时候从头播放
audioElement.currentTime = 0;
// 发布当前在播放的消息id
AzEventBus.emit('im_msg_audio_play', {mid});
};
// 暂停_回调
const handlePauseEvent = () => {
// console.log('音频暂停', mid);
};
// 如果当前audio存在就绑定事件
if (audioElement) {
audioElement.addEventListener('loadedmetadata', loadedmetadataEvent);
audioElement.addEventListener('timeupdate', timeupdateEvent);
audioElement.addEventListener('error', playErrorEvent);
audioElement.addEventListener('ended', onAudioEndedEvent);
audioElement.addEventListener('play', handlePlayEvent);
audioElement.addEventListener('pause', handlePauseEvent);
};
// 如果有别的在播放就停止当前消息播放
const imMsgAudioPlay = (msg: {mid: string | number}) => {
if (msg.mid === mid) {
return;
}
if (audioElement && audioElement.played) {
audioElement.pause();
audioElement.currentTime = 0;
setIsPlaying(false);
}
};
AzEventBus.on('im_msg_audio_play', imMsgAudioPlay);
return () => {
AzEventBus.off('im_msg_audio_play', imMsgAudioPlay);
if (!audioRef.current) {
return;
};
audioElement.removeEventListener('loadedmetadata', loadedmetadataEvent);
audioElement.removeEventListener('timeupdate', timeupdateEvent);
audioElement.removeEventListener('error', playErrorEvent);
audioElement.removeEventListener('ended', onAudioEndedEvent);
audioElement.removeEventListener('play', handlePlayEvent);
audioElement.removeEventListener('pause', handlePauseEvent);
};
}, []);
const handlePlay = () => {
if (!isPlaying) {
audioRef.current.play()
.then(() => setIsPlaying(true))
.catch(() => {
setIsPlaying(false);
});
} else {
audioRef.current.pause();
setIsPlaying(false);
}
};
return (
<div ref={currentRef} className={`${style.root} ${isSelfSend ? style.cur_audio_play_right : ''}`} style={{width: `${width}px`, height: `${height}px`}}>
<audio ref={audioRef} src={url} controls
controlsList="nodownload noplaybackrate"
preload="metadata"
style={{display: 'none'}}
>浏览器不支持该格式音频文件</audio>
<div className={`${style.cur_audio_play_wrap}`} onClick={handlePlay}>
<div className={style.cur_audio_play_time_wrap}>
<span className={style.duration}>{duration}''</span>
<span className={style.yuyin_svg}>
<IconfontSvg icons={['icon-yuyin']} className={`${style.yuyin_icon_svg} ${isPlaying ? style.yuyin_icon_svg_playing : ''}`}></IconfontSvg>
</span>
</div>
</div>
</div>
)
}
export default memo(LazyAudio);
样式
自己发消息是.cur_audio_play_right;是偏右侧的
别人发的消息是左侧;
时间和播放是使用了浮动;
.root{
text-align: left;
max-width: 300px;
min-width: 150px;
width: auto !important;
.cur_audio_play_wrap{
cursor: pointer;
display: inline-block;
background-color: #1E5D94;
height: 36px;
width: 150px;
border-radius: 0px 4px 4px 4px;
.cur_audio_play_time_wrap{
clear: both;
padding: 4px 10px 4px 10px;
color: #fff;
.duration{
float: right;
}
.yuyin_icon_svg{
width: 20px;
height: 20px;
fill: #fff;
position: relative;
top: 4px;
}
.yuyin_icon_svg_playing{
animation-name: width-animation;
animation-duration: 0.5s; /* 动画持续时间 */
animation-iteration-count: infinite; /* 无限循环 */
animation-timing-function: ease-out; /* 线性速度 */
animation-fill-mode: forwards; /* 动画结束后保持最后一帧状态 */
}
}
}
}
.cur_audio_play_right{
text-align: right;
.cur_audio_play_wrap{
background-color: #1d79cd;
clear: both;
border-radius: 4px 0px 4px 4px;
.cur_audio_play_time_wrap{
padding: 4px 10px 4px 10px;
.duration{
float: left;
}
.yuyin_icon_svg{
transform: rotate(180deg);
}
}
}
}
@keyframes width-animation {
from {
width: 0;
}
to {
width: 20px;
}
}
播放图标
阿里图标,搜索 “语音消息”; 官网地址: https://www.iconfont.cn/
Demo中使用的是svg;指定了宽度、高度、颜色; 动画使用@keyframes循环宽度变化;
@keyframes width-animation {
from {
width: 0;
}
to {
width: 20px;
}
}