https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html
环境:iOS公众号
微信版本:8+
现象: 无法停止录音,但是在Android手机中正常
主要代码如下
/**
* 语音输入组件 - 微信JS-SDK版本
*
* 提供语音识别功能,包括:
* - 录音状态管理
* - 录音动画效果
* - 错误处理
* - 结果回调
* - 微信JS-SDK语音接口集成
*/
'use client'
import { useState, useEffect, useCallback, useRef } from 'react'
import { MicrophoneIcon, XMarkIcon } from '@heroicons/react/24/outline'
import { isWechatBrowser, initWechatJsSdk, checkJsApiSupported } from '@/utils/wechat'
/**
* 防抖函数,限制函数在指定时间内只执行一次
*/
function debounce unknown>(
func: T,
wait: number
): (...args: Parameters) => void {
let timeout: NodeJS.Timeout | null = null;
return function(...args: Parameters) {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
func(...args);
timeout = null;
}, wait);
};
}
/**
* 语音输入组件的属性接口
*/
interface VoiceInputProps {
show: boolean
onClose: () => void
onResult: (text: string) => void
sendDirectly?: boolean
}
/**
* 录音状态类型
*/
type RecordingStatus = 'inactive' | 'recording' | 'processing'
// 录音相关的JS API列表
const VOICE_JS_API_LIST = [
'startRecord',
'stopRecord',
'onVoiceRecordEnd',
'translateVoice',
'uploadVoice',
];
export function VoiceInput({ show, onClose, onResult, sendDirectly = true }: VoiceInputProps) {
// 状态管理
const [status, setStatus] = useState('inactive')
const [error, setError] = useState(null)
const [isWechat, setIsWechat] = useState(false)
const [sdkInitialized, setSdkInitialized] = useState(false)
const [apiSupported, setApiSupported] = useState<{[key: string]: boolean}>({})
// 录音相关引用
const processingRef = useRef(false) // 用于跟踪处理状态,防止重复调用
const localIdRef = useRef('') // 微信语音唯一识别ID
const voiceRecordEndHandlerRef = useRef<(() => void) | null>(null); // 保存录音自动停止事件处理函数的引用
// 是否支持录音
const isRecordingSupported = apiSupported['startRecord'] && apiSupported['stopRecord'] && apiSupported['translateVoice'];
/**
* 取消录音
*/
const cancelRecording = useCallback(() => {
if (isWechat && status === 'recording') {
try {
window.wx.stopRecord();
} catch (error) {
console.error('停止录音失败:', error)
}
}
setStatus('inactive')
processingRef.current = false
onClose()
}, [onClose, status, isWechat])
/**
* 初始化微信JS-SDK
*/
useEffect(() => {
let mounted = true;
const initSdk = async () => {
const inWechat = isWechatBrowser()
if (mounted) setIsWechat(inWechat)
if (inWechat) {
try {
// 初始化JS-SDK,配置需要使用的录音接口
const initialized = await initWechatJsSdk(VOICE_JS_API_LIST)
if (mounted) setSdkInitialized(initialized)
if (initialized && mounted) {
console.log('微信JS-SDK初始化成功,准备检查接口支持情况')
// 检查当前微信版本是否支持所需接口
const supported = await checkJsApiSupported(VOICE_JS_API_LIST);
if (mounted) {
setApiSupported(supported);
// 输出支持情况日志
console.log('录音接口支持情况:',
`startRecord: ${supported['startRecord'] ? '支持' : '不支持'}, `,
`stopRecord: ${supported['stopRecord'] ? '支持' : '不支持'}, `,
`translateVoice: ${supported['translateVoice'] ? '支持' : '不支持'}`
);
// 如果不支持关键接口,显示错误提示
if (!supported['startRecord'] || !supported['stopRecord']) {
setError('当前微信版本不支持录音功能,请升级微信版本');
}
}
} else if (mounted) {
setError('微信JS-SDK初始化失败,请刷新页面重试')
}
} catch (error) {
console.error('初始化微信JS-SDK失败:', error)
if (mounted) setError('初始化微信JS-SDK失败,请刷新页面重试')
}
}
}
initSdk()
return () => {
mounted = false;
}
}, [])
/**
* 设置录音结束事件监听
*/
const setupVoiceRecordEndHandler = useCallback(() => {
if (!isWechat || !sdkInitialized || !window.wx || !isRecordingSupported) return;
// 先移除之前的事件监听(如果存在)
if (voiceRecordEndHandlerRef.current) {
// 微信JS-SDK没有提供直接的移除监听方法,但重新监听会替换之前的处理函数
voiceRecordEndHandlerRef.current();
voiceRecordEndHandlerRef.current = null;
}
// 添加新的事件监听
const handler = () => {
window.wx.onVoiceRecordEnd({
complete: function(res: { localId: string }) {
console.log('录音自动停止事件触发', res.localId);
if (status === 'recording') {
localIdRef.current = res.localId
handleRecordingComplete(res.localId)
}
}
});
};
handler(); // 立即执行一次
voiceRecordEndHandlerRef.current = handler; // 保存引用以便之后可以移除
console.log('已设置录音结束事件监听');
}, [isWechat, sdkInitialized, status, isRecordingSupported]);
/**
* 开始录音
*/
const startRecording = useCallback(async () => {
try {
setError(null)
if (isWechat && sdkInitialized) {
// 检查是否支持录音
if (!isRecordingSupported) {
setError('当前微信版本不支持录音功能,请升级微信');
return;
}
// 确保事件监听已设置
setupVoiceRecordEndHandler();
console.log('开始录音...');
// 使用微信JS-SDK开始录音
window.wx.startRecord({
success: function() {
console.log('微信录音开始成功');
setStatus('recording')
},
cancel: function () {
console.log('用户拒绝授权录音')
},
fail: function(res: { errMsg: string }) {
console.error('微信录音失败:', res.errMsg)
setError('启动录音失败: ' + res.errMsg)
}
})
} else if (!isWechat) {
setError('请在微信浏览器中使用语音功能')
} else {
setError('JS-SDK未初始化,无法使用录音功能')
}
} catch (error) {
console.error('启动录音失败:', error)
setError('启动录音失败,请确保授予麦克风权限')
}
}, [isWechat, sdkInitialized, setupVoiceRecordEndHandler, isRecordingSupported])
/**
* 停止录音 - 添加防抖处理
*/
const handleStopRecording = useCallback(() => {
// 如果不在录音状态或已经在处理中,则忽略点击
if (status !== 'recording' || processingRef.current) {
return;
}
// 设置处理标志为true,防止重复调用
processingRef.current = true;
try {
setStatus('processing')
if (isWechat && sdkInitialized && isRecordingSupported) {
console.log('停止录音...');
// 使用微信JS-SDK停止录音
window.wx.stopRecord({
success: function(res: { localId: string }) {
console.log('微信录音停止成功,localId:', res.localId);
localIdRef.current = res.localId
handleRecordingComplete(res.localId)
},
fail: function(res: { errMsg: string }) {
console.error('停止录音失败:', res.errMsg)
setError('停止录音失败: ' + res.errMsg)
setStatus('inactive')
processingRef.current = false
}
})
}
} catch (error) {
console.error('停止录音失败:', error)
setError('停止录音失败,请刷新页面后重试')
setStatus('inactive')
processingRef.current = false
}
}, [status, isWechat, sdkInitialized, isRecordingSupported])
// 防抖处理,300ms内只响应一次点击
const stopRecording = useCallback(debounce(handleStopRecording, 300), [handleStopRecording]);
/**
* 处理录音完成后的操作
*/
const handleRecordingComplete = useCallback((localId: string) => {
if (!localId) {
setError('录音失败,未获取到录音ID')
setStatus('inactive')
processingRef.current = false
return
}
if (!apiSupported['translateVoice']) {
setError('当前微信版本不支持语音识别功能,请升级微信');
setStatus('inactive')
processingRef.current = false
return
}
console.log('处理录音完成,开始识别,localId:', localId);
// 使用微信内置的语音识别接口
window.wx.translateVoice({
localId: localId,
isShowProgressTips: 1, // 显示进度提示
success: function(res: { translateResult: string }) {
console.log('微信语音识别成功:', res.translateResult);
if (res.translateResult && res.translateResult.trim()) {
onResult(res.translateResult.trim())
// 如果是直接发送模式,处理完成后关闭弹窗
if (sendDirectly) {
onClose()
}
} else {
console.log('微信语音识别结果为空,尝试通过服务器识别');
if (apiSupported['uploadVoice']) {
// 如果微信识别失败,尝试通过服务器端识别
uploadVoiceToServer(localId)
} else {
setError('语音识别失败,当前微信版本不支持语音上传');
setStatus('inactive')
processingRef.current = false
}
}
},
fail: function(res: { errMsg: string }) {
console.error('微信语音识别失败:', res.errMsg)
if (apiSupported['uploadVoice']) {
// 如果微信自带语音识别失败,尝试通过上传到服务器识别
uploadVoiceToServer(localId)
} else {
setError('语音识别失败,当前微信版本不支持语音上传');
setStatus('inactive')
processingRef.current = false
}
}
})
}, [onResult, onClose, sendDirectly, apiSupported])
/**
* 上传语音到服务器进行识别
*/
const uploadVoiceToServer = useCallback(async (localId: string) => {
try {
console.log('开始上传语音到服务器,localId:', localId);
// 先将语音上传到微信服务器获取serverId
window.wx.uploadVoice({
localId: localId,
isShowProgressTips: 1,
success: async function(res: { serverId: string }) {
console.log('语音上传到微信服务器成功,serverId:', res.serverId);
try {
// 通过API将微信服务器的语音转换为文本
// 这里需要后端提供一个接口,接收serverId,然后通过微信公众号接口获取语音文件并转换为文本
const response = await fetch('/api/wechat/voice-to-text', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
mediaId: res.serverId
})
})
if (!response.ok) {
throw new Error(`服务器识别失败: ${response.status}`)
}
const data = await response.json()
console.log('服务器语音识别结果:', data);
if (data.text && data.text.trim()) {
onResult(data.text.trim())
// 如果是直接发送模式,处理完成后关闭弹窗
if (sendDirectly) {
onClose()
}
} else {
setError('未能识别语音内容,请重新尝试')
}
} catch (error) {
console.error('服务器语音识别失败:', error)
setError('语音识别失败,请重试')
} finally {
setStatus('inactive')
processingRef.current = false
}
},
fail: function(res: { errMsg: string }) {
console.error('上传语音失败:', res.errMsg)
setError('上传语音失败: ' + res.errMsg)
setStatus('inactive')
processingRef.current = false
}
})
} catch (error) {
console.error('处理录音失败:', error)
setError('处理录音失败,请重试')
setStatus('inactive')
processingRef.current = false
}
}, [onResult, onClose, sendDirectly])
// 当组件显示或隐藏状态变化时重置组件状态
useEffect(() => {
if (show) {
// 如果组件显示,且状态是非活动的,则开始录音
if (status === 'inactive' && !processingRef.current) {
console.log('VoiceInput组件显示,准备开始录音');
// 清空之前的状态
setError(null);
localIdRef.current = '';
// 延迟一点开始录音,确保UI已经完全渲染
setTimeout(() => {
startRecording();
}, 300);
}
} else {
// 如果组件隐藏,但仍在录音中,则停止录音
if (status === 'recording' && isWechat && sdkInitialized && isRecordingSupported) {
try {
console.log('VoiceInput组件隐藏,停止录音');
window.wx.stopRecord();
} catch (error) {
console.error('停止录音失败:', error);
}
}
// 组件隐藏时重置状态
setStatus('inactive');
processingRef.current = false;
}
}, [show, status, isWechat, sdkInitialized, startRecording, isRecordingSupported]);
// 组件卸载时清理资源
useEffect(() => {
return () => {
// 如果正在录音,停止录音
if (status === 'recording' && isWechat && sdkInitialized && isRecordingSupported) {
try {
window.wx.stopRecord();
} catch (error) {
console.error('清理时停止录音失败:', error);
}
}
// 确保标志被重置
processingRef.current = false;
};
}, [status, isWechat, sdkInitialized, isRecordingSupported]);
if (!show) return null
return (
{/* 关闭按钮 - 在处理中时禁用 */}
{/* 标题 */}
{isWechat ? '微信语音输入' : '语音输入'}
{/* 录音按钮 - 已修改为停止录音按钮 */}
{status === 'recording' ? (
) : status === 'processing' ? (
) : (
)}
{/* 状态提示 */}
{status === 'inactive' && isWechat && isRecordingSupported ? '准备录音...' :
status === 'inactive' && isWechat && !isRecordingSupported ? '当前微信版本不支持录音功能' :
status === 'inactive' && !isWechat ? '请在微信中使用此功能' : ''}
{status === 'recording' && '正在录音...点击停止'}
{status === 'processing' && '正在识别语音...'}
{/* 错误提示 */}
{error && (
{error}
)}
{/* 使用提示 */}
{isWechat && isRecordingSupported ?
'使用微信语音接口识别,准确度更高' :
isWechat && !isRecordingSupported ?
'当前微信版本不支持录音,请升级微信版本' :
'请在微信浏览器中打开此页面使用语音功能'}
)
}
你好,麻烦提供下机型,微信版本号和复现链接