Commit f4859448 authored by 水玉婷's avatar 水玉婷
Browse files

feat:更新语音组件交互

parent 54a022a9
...@@ -10,11 +10,11 @@ ...@@ -10,11 +10,11 @@
'recording': isRecording, 'recording': isRecording,
'disabled': disabled 'disabled': disabled
}" }"
@click="toggleFullScreen" @click="handleClick"
@mousedown="startRecording" @mousedown="handleMouseDown"
@mouseup="stopRecording" @mouseup="stopRecording"
@mouseleave="stopRecording" @mouseleave="stopRecording"
@touchstart="startRecording" @touchstart="handleTouchStart"
@touchend="stopRecording" @touchend="stopRecording"
@touchcancel="stopRecording" @touchcancel="stopRecording"
:disabled="disabled" :disabled="disabled"
...@@ -91,6 +91,57 @@ const mediaRecorder = ref<MediaRecorder | null>(null) ...@@ -91,6 +91,57 @@ const mediaRecorder = ref<MediaRecorder | null>(null)
const audioChunks = ref<Blob[]>([]) const audioChunks = ref<Blob[]>([])
const audioStream = ref<MediaStream | null>(null) const audioStream = ref<MediaStream | null>(null)
// 全局音频流缓存(单例模式)
let globalAudioStream: MediaStream | null = null
// 获取或创建音频流(单例模式)
const getOrCreateAudioStream = async (): Promise<MediaStream> => {
// 检查是否有活跃的音频流
if (globalAudioStream) {
// 检查流是否仍然活跃
const activeTracks = globalAudioStream.getAudioTracks().filter(track => track.readyState === 'live')
if (activeTracks.length > 0) {
if (props.debug) {
console.log('复用现有音频流')
}
return globalAudioStream
} else {
// 流已失效,清理缓存
globalAudioStream = null
}
}
// 创建新的音频流
if (props.debug) {
console.log('创建新的音频流')
}
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
sampleRate: 44100
}
})
// 缓存音频流
globalAudioStream = stream
// 监听轨道结束事件,清理缓存
stream.getAudioTracks().forEach(track => {
track.addEventListener('ended', () => {
if (globalAudioStream === stream) {
globalAudioStream = null
if (props.debug) {
console.log('音频流已结束,清理缓存')
}
}
})
})
return stream
}
// 格式化录音时长显示(分:秒) // 格式化录音时长显示(分:秒)
const formatDuration = (seconds: number) => { const formatDuration = (seconds: number) => {
const minutes = Math.floor(seconds / 60) const minutes = Math.floor(seconds / 60)
...@@ -134,6 +185,67 @@ const exitFullScreen = () => { ...@@ -134,6 +185,67 @@ const exitFullScreen = () => {
isFullScreen.value = false isFullScreen.value = false
} }
// 点击事件处理
const handleClick = () => {
if (props.disabled || isRecording.value) return
// 如果不在全屏模式,进入全屏模式
if (!isFullScreen.value) {
isFullScreen.value = true
// 在企业微信中,进入全屏时异步预加载音频流
// 这样用户点击"按住说话"时就不会再弹出授权对话框
if (props.debug) {
console.log('进入全屏模式,异步预加载音频流')
}
// 异步预加载音频流,不阻塞UI
setTimeout(async () => {
try {
// 预加载音频流,但不开始录音
const stream = await getOrCreateAudioStream()
if (props.debug) {
console.log('音频流预加载完成')
}
// 预加载成功后,立即停止音频轨道,避免自动开始录音
// 但保持流缓存,这样用户点击"按住说话"时可以直接复用
stream.getAudioTracks().forEach(track => {
track.stop()
})
// 清理缓存,因为轨道已停止
globalAudioStream = null
if (props.debug) {
console.log('音频流预加载完成,轨道已停止,缓存已清理')
}
} catch (error) {
if (props.debug) {
console.warn('音频流预加载失败:', error)
}
// 预加载失败不影响后续操作,用户点击"按住说话"时会重新尝试
}
}, 0)
}
}
// 处理鼠标按下事件
const handleMouseDown = (event: MouseEvent) => {
if (props.debug) {
console.log('鼠标按下事件触发')
}
startRecording()
}
// 处理触摸开始事件
const handleTouchStart = (event: TouchEvent) => {
if (props.debug) {
console.log('触摸开始事件触发')
}
startRecording()
}
// 检查浏览器是否支持MediaRecorder // 检查浏览器是否支持MediaRecorder
const isMediaRecorderSupported = () => { const isMediaRecorderSupported = () => {
const supported = 'MediaRecorder' in window; const supported = 'MediaRecorder' in window;
...@@ -186,14 +298,8 @@ const startRecording = async () => { ...@@ -186,14 +298,8 @@ const startRecording = async () => {
} }
try { try {
// 获取麦克风权限 // 使用缓存音频流(单例模式)
const stream = await navigator.mediaDevices.getUserMedia({ const stream = await getOrCreateAudioStream();
audio: {
echoCancellation: true,
noiseSuppression: true,
sampleRate: 44100
}
});
audioStream.value = stream; audioStream.value = stream;
audioChunks.value = []; audioChunks.value = [];
...@@ -275,8 +381,8 @@ const stopRecording = async () => { ...@@ -275,8 +381,8 @@ const stopRecording = async () => {
// 停止录音计时器 // 停止录音计时器
stopRecordingTimer(); stopRecordingTimer();
// 停止所有音频轨道 // 停止音频轨道,释放麦克风权限
if (audioStream.value) { if (audioStream.value) {
audioStream.value.getTracks().forEach(track => track.stop()); audioStream.value.getTracks().forEach(track => track.stop());
audioStream.value = null; audioStream.value = null;
...@@ -315,7 +421,7 @@ const uploadAudioFile = async (audioBlob: Blob): Promise<{filePath: string, dura ...@@ -315,7 +421,7 @@ const uploadAudioFile = async (audioBlob: Blob): Promise<{filePath: string, dura
// 解析响应体为JSON // 解析响应体为JSON
const result = await response.json(); const result = await response.json();
if (result.data.code === 0) { if (result.code === 0) {
const filePath = result.data.filePath; const filePath = result.data.filePath;
// 计算音频时长(秒),四舍五入取整 // 计算音频时长(秒),四舍五入取整
const durationTime = recordingDuration.value; const durationTime = recordingDuration.value;
...@@ -331,6 +437,17 @@ const uploadAudioFile = async (audioBlob: Blob): Promise<{filePath: string, dura ...@@ -331,6 +437,17 @@ const uploadAudioFile = async (audioBlob: Blob): Promise<{filePath: string, dura
// 发送录制的音频 // 发送录制的音频
const sendRecordedAudio = async () => { const sendRecordedAudio = async () => {
// 防误触检查:只有录音时长超过0.5秒才发送
if (recordingDuration.value < 0.5) {
if (props.debug) {
console.log('录音时长过短,视为误触,不发送数据。时长:', recordingDuration.value, '');
}
// 清理录音数据并退出全屏
audioChunks.value = [];
exitFullScreen();
return;
}
if (audioChunks.value.length === 0) { if (audioChunks.value.length === 0) {
emit('error', '录音数据为空'); emit('error', '录音数据为空');
exitFullScreen(); // 数据为空时退出全屏 exitFullScreen(); // 数据为空时退出全屏
...@@ -358,6 +475,19 @@ const sendRecordedAudio = async () => { ...@@ -358,6 +475,19 @@ const sendRecordedAudio = async () => {
} finally { } finally {
// 清理录音数据并退出全屏 // 清理录音数据并退出全屏
audioChunks.value = []; audioChunks.value = [];
// 清理音频流,释放麦克风权限(重要:解决企业微信中麦克风一直开启的问题)
// 但保留全局缓存,以便后续录音可以复用
if (audioStream.value) {
// 只停止轨道,不清理全局缓存,以便后续复用
audioStream.value.getTracks().forEach(track => track.stop());
audioStream.value = null;
if (props.debug) {
console.log('消息发送完成:音频流轨道已停止,麦克风权限已释放');
}
}
exitFullScreen(); exitFullScreen();
} }
} }
...@@ -368,10 +498,21 @@ onUnmounted(() => { ...@@ -368,10 +498,21 @@ onUnmounted(() => {
stopRecording(); stopRecording();
} }
// 清理当前录音音频流
if (audioStream.value) { if (audioStream.value) {
audioStream.value.getTracks().forEach(track => track.stop()); audioStream.value.getTracks().forEach(track => track.stop());
} }
// 清理全局缓存音频流(重要:防止权限占用)
if (globalAudioStream) {
globalAudioStream.getTracks().forEach(track => track.stop());
globalAudioStream = null;
if (props.debug) {
console.log('组件卸载:全局音频流已清理');
}
}
// 清理计时器 // 清理计时器
stopRecordingTimer(); stopRecordingTimer();
}) })
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment