You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
223 lines
4.6 KiB
Vue
223 lines
4.6 KiB
Vue
|
5 months ago
|
<template>
|
||
|
|
<div
|
||
|
|
:class="!msg.isSelf || mode === 'audio-in' ? 'audio-in' : 'audio-out'"
|
||
|
|
:style="{ width: audioContainerWidth + 'px' }"
|
||
|
|
@tap="onPlayAudio"
|
||
|
|
>
|
||
|
|
<div class="audio-dur">{{ duration }}s</div>
|
||
|
|
<div class="audio-icon-wrapper">
|
||
|
|
<Icon :size="24" :key="audioIconType" :type="audioIconType" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script lang="ts" setup>
|
||
|
|
import {
|
||
|
|
ref,
|
||
|
|
onUnmounted,
|
||
|
|
computed,
|
||
|
|
watch,
|
||
|
|
withDefaults,
|
||
|
|
} from '../../../utils/transformVue'
|
||
|
|
import Icon from '../../../components/Icon.vue'
|
||
|
|
import { events } from '../../../utils/constants'
|
||
|
|
import { V2NIMMessageForUI } from '@xkit-yx/im-store-v2/dist/types/types'
|
||
|
|
import { V2NIMMessageAudioAttachment } from 'nim-web-sdk-ng/dist/v2/NIM_UNIAPP_SDK/V2NIMMessageService'
|
||
|
|
import { onHide, onUnload } from '@dcloudio/uni-app'
|
||
|
|
|
||
|
|
const props = withDefaults(
|
||
|
|
defineProps<{
|
||
|
|
msg: V2NIMMessageForUI
|
||
|
|
mode?: 'audio-in' | 'audio-out'
|
||
|
|
broadcastNewAudioSrc?: string
|
||
|
|
}>(),
|
||
|
|
{}
|
||
|
|
)
|
||
|
|
|
||
|
|
const audioIconType = ref('icon-yuyin3')
|
||
|
|
const animationFlag = ref(false)
|
||
|
|
const isAudioPlaying = ref<boolean>(false)
|
||
|
|
const audioMap = new Map<string, any>()
|
||
|
|
const emits = defineEmits(['getGlobalAudioContext'])
|
||
|
|
|
||
|
|
// 格式化音频时长
|
||
|
|
const formatDuration = (duration: number) => {
|
||
|
|
return Math.round(duration / 1000) || 1
|
||
|
|
}
|
||
|
|
|
||
|
|
// 音频消息宽度
|
||
|
|
const audioContainerWidth = computed(() => {
|
||
|
|
const duration = formatDuration(props.msg.attachment?.duration)
|
||
|
|
const maxWidth = 180
|
||
|
|
return 50 + 8 * (duration - 1) > maxWidth ? maxWidth : 50 + 8 * (duration - 1)
|
||
|
|
})
|
||
|
|
|
||
|
|
// 音频时长
|
||
|
|
const duration = computed(() => {
|
||
|
|
return formatDuration(
|
||
|
|
(props.msg.attachment as V2NIMMessageAudioAttachment)?.duration
|
||
|
|
)
|
||
|
|
})
|
||
|
|
|
||
|
|
// 播发音频
|
||
|
|
const onPlayAudio = () => {
|
||
|
|
uni.$emit(events.AUDIO_URL_CHANGE, props.msg?.attachment?.url)
|
||
|
|
const audioContext = getAudio()
|
||
|
|
if (!audioContext) {
|
||
|
|
const globalAudioContext = uni.createInnerAudioContext()
|
||
|
|
audioMap.set('audio', globalAudioContext)
|
||
|
|
initAudioSrc()
|
||
|
|
}
|
||
|
|
toggleAudioPlayState()
|
||
|
|
}
|
||
|
|
|
||
|
|
watch(
|
||
|
|
() => props.broadcastNewAudioSrc,
|
||
|
|
(newSrc) => {
|
||
|
|
if (newSrc !== props.msg?.attachment?.url && isAudioPlaying.value) {
|
||
|
|
stopAudio()
|
||
|
|
isAudioPlaying.value = false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
const toggleAudioPlayState = () => {
|
||
|
|
if (!isAudioPlaying.value) {
|
||
|
|
playAudio()
|
||
|
|
} else {
|
||
|
|
stopAudio()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 初始化音频实例
|
||
|
|
function initAudioSrc() {
|
||
|
|
const audioContext = getAudio()
|
||
|
|
if (!audioContext) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
audioContext.src = props.msg?.attachment?.url
|
||
|
|
isAudioPlaying.value = false
|
||
|
|
audioContext.onPlay(onAudioPlay)
|
||
|
|
audioContext.onStop(onAudioStop)
|
||
|
|
audioContext.onEnded(onAudioEnded)
|
||
|
|
audioContext.onError(onAudioError)
|
||
|
|
}
|
||
|
|
|
||
|
|
function playAudio() {
|
||
|
|
const audioContext = getAudio()
|
||
|
|
if (!audioContext) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
audioContext.play()
|
||
|
|
}
|
||
|
|
|
||
|
|
function onAudioPlay() {
|
||
|
|
isAudioPlaying.value = true
|
||
|
|
playAudioAnimation()
|
||
|
|
}
|
||
|
|
|
||
|
|
function onAudioStop() {
|
||
|
|
animationFlag.value = false
|
||
|
|
isAudioPlaying.value = false
|
||
|
|
}
|
||
|
|
|
||
|
|
function onAudioEnded() {
|
||
|
|
animationFlag.value = false
|
||
|
|
isAudioPlaying.value = false
|
||
|
|
}
|
||
|
|
|
||
|
|
function onAudioError() {
|
||
|
|
animationFlag.value = false
|
||
|
|
console.warn('audio played error')
|
||
|
|
}
|
||
|
|
|
||
|
|
const getAudio = () => {
|
||
|
|
return audioMap.get('audio')
|
||
|
|
}
|
||
|
|
|
||
|
|
const stopAudio = () => {
|
||
|
|
const audioContext = getAudio()
|
||
|
|
if (!audioContext) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
audioContext.stop()
|
||
|
|
} catch {
|
||
|
|
// console.log('stop audio error')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 播放音频动画
|
||
|
|
const playAudioAnimation = () => {
|
||
|
|
try {
|
||
|
|
animationFlag.value = true
|
||
|
|
let audioIcons = ['icon-yuyin1', 'icon-yuyin2', 'icon-yuyin3']
|
||
|
|
const handler = () => {
|
||
|
|
const icon = audioIcons.shift()
|
||
|
|
if (icon) {
|
||
|
|
audioIconType.value = icon
|
||
|
|
if (!audioIcons.length && animationFlag.value) {
|
||
|
|
audioIcons = ['icon-yuyin1', 'icon-yuyin2', 'icon-yuyin3']
|
||
|
|
}
|
||
|
|
if (audioIcons.length) {
|
||
|
|
setTimeout(handler, 300)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
handler()
|
||
|
|
} catch (error) {
|
||
|
|
// console.log('playAudioAnimation error', error)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const stopAudioOnHide = () => {
|
||
|
|
const audioContext = getAudio()
|
||
|
|
if (isAudioPlaying.value) {
|
||
|
|
stopAudio()
|
||
|
|
}
|
||
|
|
audioContext?.destroy?.()
|
||
|
|
animationFlag.value = false
|
||
|
|
audioMap.delete('audio')
|
||
|
|
}
|
||
|
|
|
||
|
|
onUnmounted(() => {
|
||
|
|
stopAudioOnHide()
|
||
|
|
})
|
||
|
|
|
||
|
|
onHide(() => {
|
||
|
|
stopAudioOnHide()
|
||
|
|
})
|
||
|
|
|
||
|
|
onUnload(() => {
|
||
|
|
stopAudioOnHide()
|
||
|
|
})
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style scoped lang="scss">
|
||
|
|
.audio-dur {
|
||
|
|
height: 24px;
|
||
|
|
line-height: 24px;
|
||
|
|
}
|
||
|
|
.audio-in,
|
||
|
|
.audio-out {
|
||
|
|
width: 50px;
|
||
|
|
display: flex;
|
||
|
|
cursor: pointer;
|
||
|
|
justify-content: flex-end;
|
||
|
|
align-items: center;
|
||
|
|
}
|
||
|
|
|
||
|
|
.audio-in {
|
||
|
|
flex-direction: row-reverse;
|
||
|
|
.audio-icon-wrapper {
|
||
|
|
transform: rotate(180deg);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.audio-icon-wrapper {
|
||
|
|
height: 24px;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
}
|
||
|
|
</style>
|