751 lines
19 KiB
JavaScript
751 lines
19 KiB
JavaScript
// 语音消息管理器 - 微信小程序专用
|
|
// 处理语音录制、播放、转换、上传等功能
|
|
|
|
const apiClient = require('./api-client.js');
|
|
const performanceMonitor = require('./performance-monitor.js');
|
|
|
|
/**
|
|
* 语音消息管理器
|
|
* 功能:
|
|
* 1. 语音录制和停止
|
|
* 2. 语音播放和暂停
|
|
* 3. 语音文件管理
|
|
* 4. 语音质量控制
|
|
* 5. 语音时长限制
|
|
* 6. 语音格式转换
|
|
*/
|
|
class VoiceMessageManager {
|
|
constructor() {
|
|
this.isInitialized = false;
|
|
|
|
// 录音配置
|
|
this.recordConfig = {
|
|
duration: 60000, // 最大录音时长 60秒
|
|
sampleRate: 16000, // 采样率
|
|
numberOfChannels: 1, // 声道数
|
|
encodeBitRate: 48000, // 编码码率
|
|
format: 'mp3', // 录音格式
|
|
frameSize: 50, // 帧大小
|
|
minDuration: 1000, // 最小录音时长 1秒
|
|
maxDuration: 60000 // 最大录音时长 60秒
|
|
};
|
|
|
|
// 播放配置
|
|
this.playConfig = {
|
|
autoplay: false,
|
|
loop: false,
|
|
volume: 1.0,
|
|
playbackRate: 1.0
|
|
};
|
|
|
|
// 录音器实例
|
|
this.recorderManager = null;
|
|
this.innerAudioContext = null;
|
|
|
|
// 录音状态
|
|
this.recordingState = {
|
|
isRecording: false,
|
|
isPaused: false,
|
|
startTime: null,
|
|
duration: 0,
|
|
tempFilePath: null,
|
|
fileSize: 0
|
|
};
|
|
|
|
// 播放状态
|
|
this.playingState = {
|
|
isPlaying: false,
|
|
isPaused: false,
|
|
currentTime: 0,
|
|
duration: 0,
|
|
currentVoiceId: null,
|
|
playingMessageId: null
|
|
};
|
|
|
|
// 语音文件缓存
|
|
this.voiceCache = new Map();
|
|
|
|
// 事件监听器
|
|
this.eventListeners = new Map();
|
|
|
|
// 权限状态
|
|
this.permissionGranted = false;
|
|
|
|
this.init();
|
|
}
|
|
|
|
// 初始化语音消息管理器
|
|
async init() {
|
|
if (this.isInitialized) return;
|
|
|
|
console.log('🎤 初始化语音消息管理器...');
|
|
|
|
try {
|
|
// 初始化录音管理器
|
|
this.initRecorderManager();
|
|
|
|
// 初始化音频播放器
|
|
this.initAudioPlayer();
|
|
|
|
// 检查录音权限
|
|
await this.checkRecordPermission();
|
|
|
|
this.isInitialized = true;
|
|
console.log('✅ 语音消息管理器初始化完成');
|
|
|
|
} catch (error) {
|
|
console.error('❌ 语音消息管理器初始化失败:', error);
|
|
}
|
|
}
|
|
|
|
// 初始化录音管理器
|
|
initRecorderManager() {
|
|
this.recorderManager = wx.getRecorderManager();
|
|
|
|
// 录音开始事件
|
|
this.recorderManager.onStart(() => {
|
|
console.log('🎤 录音开始');
|
|
this.recordingState.isRecording = true;
|
|
this.recordingState.startTime = Date.now();
|
|
this.triggerEvent('recordStart');
|
|
});
|
|
|
|
// 录音暂停事件
|
|
this.recorderManager.onPause(() => {
|
|
console.log('⏸️ 录音暂停');
|
|
this.recordingState.isPaused = true;
|
|
this.triggerEvent('recordPause');
|
|
});
|
|
|
|
// 录音恢复事件
|
|
this.recorderManager.onResume(() => {
|
|
console.log('▶️ 录音恢复');
|
|
this.recordingState.isPaused = false;
|
|
this.triggerEvent('recordResume');
|
|
});
|
|
|
|
// 录音停止事件
|
|
this.recorderManager.onStop((res) => {
|
|
console.log('⏹️ 录音停止:', res);
|
|
|
|
this.recordingState.isRecording = false;
|
|
this.recordingState.isPaused = false;
|
|
this.recordingState.duration = res.duration;
|
|
this.recordingState.tempFilePath = res.tempFilePath;
|
|
this.recordingState.fileSize = res.fileSize;
|
|
|
|
this.triggerEvent('recordStop', {
|
|
duration: res.duration,
|
|
tempFilePath: res.tempFilePath,
|
|
fileSize: res.fileSize
|
|
});
|
|
});
|
|
|
|
// 录音错误事件
|
|
this.recorderManager.onError((error) => {
|
|
console.error('❌ 录音错误:', error);
|
|
this.recordingState.isRecording = false;
|
|
this.recordingState.isPaused = false;
|
|
this.triggerEvent('recordError', error);
|
|
});
|
|
|
|
// 录音帧数据事件(用于实时波形显示)
|
|
this.recorderManager.onFrameRecorded((res) => {
|
|
this.triggerEvent('recordFrame', {
|
|
frameBuffer: res.frameBuffer,
|
|
isLastFrame: res.isLastFrame
|
|
});
|
|
});
|
|
}
|
|
|
|
// 初始化音频播放器
|
|
initAudioPlayer() {
|
|
this.innerAudioContext = wx.createInnerAudioContext();
|
|
|
|
// 播放开始事件
|
|
this.innerAudioContext.onPlay(() => {
|
|
console.log('🔊 语音播放开始');
|
|
this.playingState.isPlaying = true;
|
|
this.playingState.isPaused = false;
|
|
this.triggerEvent('playStart');
|
|
});
|
|
|
|
// 播放暂停事件
|
|
this.innerAudioContext.onPause(() => {
|
|
console.log('⏸️ 语音播放暂停');
|
|
this.playingState.isPaused = true;
|
|
this.triggerEvent('playPause');
|
|
});
|
|
|
|
// 播放结束事件
|
|
this.innerAudioContext.onEnded(() => {
|
|
console.log('⏹️ 语音播放结束');
|
|
this.playingState.isPlaying = false;
|
|
this.playingState.isPaused = false;
|
|
this.playingState.currentTime = 0;
|
|
this.playingState.currentVoiceId = null;
|
|
this.playingState.playingMessageId = null;
|
|
this.triggerEvent('playEnd');
|
|
});
|
|
|
|
// 播放错误事件
|
|
this.innerAudioContext.onError((error) => {
|
|
console.error('❌ 语音播放错误:', error);
|
|
this.playingState.isPlaying = false;
|
|
this.playingState.isPaused = false;
|
|
this.triggerEvent('playError', error);
|
|
});
|
|
|
|
// 播放进度更新事件
|
|
this.innerAudioContext.onTimeUpdate(() => {
|
|
this.playingState.currentTime = this.innerAudioContext.currentTime;
|
|
this.playingState.duration = this.innerAudioContext.duration;
|
|
this.triggerEvent('playTimeUpdate', {
|
|
currentTime: this.playingState.currentTime,
|
|
duration: this.playingState.duration
|
|
});
|
|
});
|
|
|
|
// 音频加载完成事件
|
|
this.innerAudioContext.onCanplay(() => {
|
|
console.log('🎵 语音加载完成');
|
|
this.triggerEvent('playCanplay');
|
|
});
|
|
}
|
|
|
|
// 🎤 ===== 录音功能 =====
|
|
|
|
// 开始录音
|
|
async startRecording(options = {}) {
|
|
if (!this.isInitialized) {
|
|
throw new Error('语音消息管理器未初始化');
|
|
}
|
|
|
|
if (this.recordingState.isRecording) {
|
|
throw new Error('正在录音中');
|
|
}
|
|
|
|
// 检查录音权限
|
|
if (!this.permissionGranted) {
|
|
const granted = await this.requestRecordPermission();
|
|
if (!granted) {
|
|
throw new Error('录音权限被拒绝');
|
|
}
|
|
}
|
|
|
|
try {
|
|
const recordOptions = {
|
|
...this.recordConfig,
|
|
...options
|
|
};
|
|
|
|
console.log('🎤 开始录音,配置:', recordOptions);
|
|
|
|
// 重置录音状态
|
|
this.recordingState = {
|
|
isRecording: false,
|
|
isPaused: false,
|
|
startTime: null,
|
|
duration: 0,
|
|
tempFilePath: null,
|
|
fileSize: 0
|
|
};
|
|
|
|
this.recorderManager.start(recordOptions);
|
|
|
|
} catch (error) {
|
|
console.error('❌ 开始录音失败:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// 停止录音
|
|
stopRecording() {
|
|
if (!this.recordingState.isRecording) {
|
|
throw new Error('当前没有在录音');
|
|
}
|
|
|
|
try {
|
|
console.log('⏹️ 停止录音');
|
|
this.recorderManager.stop();
|
|
|
|
} catch (error) {
|
|
console.error('❌ 停止录音失败:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// 暂停录音
|
|
pauseRecording() {
|
|
if (!this.recordingState.isRecording || this.recordingState.isPaused) {
|
|
throw new Error('当前状态无法暂停录音');
|
|
}
|
|
|
|
try {
|
|
console.log('⏸️ 暂停录音');
|
|
this.recorderManager.pause();
|
|
|
|
} catch (error) {
|
|
console.error('❌ 暂停录音失败:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// 恢复录音
|
|
resumeRecording() {
|
|
if (!this.recordingState.isRecording || !this.recordingState.isPaused) {
|
|
throw new Error('当前状态无法恢复录音');
|
|
}
|
|
|
|
try {
|
|
console.log('▶️ 恢复录音');
|
|
this.recorderManager.resume();
|
|
|
|
} catch (error) {
|
|
console.error('❌ 恢复录音失败:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// 取消录音
|
|
cancelRecording() {
|
|
if (!this.recordingState.isRecording) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
console.log('❌ 取消录音');
|
|
this.recorderManager.stop();
|
|
|
|
// 重置录音状态
|
|
this.recordingState = {
|
|
isRecording: false,
|
|
isPaused: false,
|
|
startTime: null,
|
|
duration: 0,
|
|
tempFilePath: null,
|
|
fileSize: 0
|
|
};
|
|
|
|
this.triggerEvent('recordCancel');
|
|
|
|
} catch (error) {
|
|
console.error('❌ 取消录音失败:', error);
|
|
}
|
|
}
|
|
|
|
// 🔊 ===== 播放功能 =====
|
|
|
|
// 播放语音消息
|
|
async playVoiceMessage(voiceUrl, messageId = null, options = {}) {
|
|
if (!this.isInitialized) {
|
|
throw new Error('语音消息管理器未初始化');
|
|
}
|
|
|
|
try {
|
|
// 如果正在播放其他语音,先停止
|
|
if (this.playingState.isPlaying) {
|
|
this.stopPlaying();
|
|
}
|
|
|
|
console.log('🔊 播放语音消息:', voiceUrl);
|
|
|
|
// 设置播放配置
|
|
const playOptions = {
|
|
...this.playConfig,
|
|
...options
|
|
};
|
|
|
|
this.innerAudioContext.src = voiceUrl;
|
|
this.innerAudioContext.autoplay = playOptions.autoplay;
|
|
this.innerAudioContext.loop = playOptions.loop;
|
|
this.innerAudioContext.volume = playOptions.volume;
|
|
this.innerAudioContext.playbackRate = playOptions.playbackRate;
|
|
|
|
// 更新播放状态
|
|
this.playingState.currentVoiceId = voiceUrl;
|
|
this.playingState.playingMessageId = messageId;
|
|
|
|
// 开始播放
|
|
this.innerAudioContext.play();
|
|
|
|
} catch (error) {
|
|
console.error('❌ 播放语音消息失败:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// 暂停播放
|
|
pausePlaying() {
|
|
if (!this.playingState.isPlaying || this.playingState.isPaused) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
console.log('⏸️ 暂停播放');
|
|
this.innerAudioContext.pause();
|
|
|
|
} catch (error) {
|
|
console.error('❌ 暂停播放失败:', error);
|
|
}
|
|
}
|
|
|
|
// 恢复播放
|
|
resumePlaying() {
|
|
if (!this.playingState.isPlaying || !this.playingState.isPaused) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
console.log('▶️ 恢复播放');
|
|
this.innerAudioContext.play();
|
|
|
|
} catch (error) {
|
|
console.error('❌ 恢复播放失败:', error);
|
|
}
|
|
}
|
|
|
|
// 停止播放
|
|
stopPlaying() {
|
|
if (!this.playingState.isPlaying) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
console.log('⏹️ 停止播放');
|
|
this.innerAudioContext.stop();
|
|
|
|
// 重置播放状态
|
|
this.playingState.isPlaying = false;
|
|
this.playingState.isPaused = false;
|
|
this.playingState.currentTime = 0;
|
|
this.playingState.currentVoiceId = null;
|
|
this.playingState.playingMessageId = null;
|
|
|
|
} catch (error) {
|
|
console.error('❌ 停止播放失败:', error);
|
|
}
|
|
}
|
|
|
|
// 设置播放进度
|
|
seekTo(time) {
|
|
if (!this.playingState.isPlaying) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
this.innerAudioContext.seek(time);
|
|
this.playingState.currentTime = time;
|
|
|
|
} catch (error) {
|
|
console.error('❌ 设置播放进度失败:', error);
|
|
}
|
|
}
|
|
|
|
// 📁 ===== 文件管理 =====
|
|
|
|
// 上传语音文件
|
|
async uploadVoiceFile(tempFilePath, duration) {
|
|
try {
|
|
console.log('📤 上传语音文件:', tempFilePath);
|
|
|
|
// 使用微信小程序的上传文件API
|
|
const uploadResult = await new Promise((resolve, reject) => {
|
|
wx.uploadFile({
|
|
url: `${apiClient.baseUrl}/api/v1/file/upload`,
|
|
filePath: tempFilePath,
|
|
name: 'file',
|
|
formData: {
|
|
file_type: 'audio',
|
|
usage_type: 'message',
|
|
duration: duration.toString()
|
|
},
|
|
header: {
|
|
'Authorization': `Bearer ${apiClient.getToken()}`
|
|
},
|
|
success: (res) => {
|
|
try {
|
|
const data = JSON.parse(res.data);
|
|
resolve({
|
|
success: data.success || res.statusCode === 200,
|
|
data: data.data || data,
|
|
message: data.message
|
|
});
|
|
} catch (error) {
|
|
resolve({
|
|
success: res.statusCode === 200,
|
|
data: { url: res.data },
|
|
message: '上传成功'
|
|
});
|
|
}
|
|
},
|
|
fail: reject
|
|
});
|
|
});
|
|
|
|
if (uploadResult.success) {
|
|
const fileUrl = uploadResult.data.url || uploadResult.data.file_url || uploadResult.data.fileUrl;
|
|
console.log('✅ 语音文件上传成功:', fileUrl);
|
|
return {
|
|
success: true,
|
|
url: fileUrl,
|
|
duration: duration,
|
|
size: this.recordingState.fileSize
|
|
};
|
|
} else {
|
|
throw new Error(uploadResult.message || '上传失败');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('❌ 上传语音文件失败:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// 下载语音文件到本地
|
|
async downloadVoiceFile(voiceUrl) {
|
|
try {
|
|
// 检查缓存
|
|
if (this.voiceCache.has(voiceUrl)) {
|
|
const cached = this.voiceCache.get(voiceUrl);
|
|
if (this.isFileExists(cached.localPath)) {
|
|
return cached.localPath;
|
|
} else {
|
|
this.voiceCache.delete(voiceUrl);
|
|
}
|
|
}
|
|
|
|
console.log('📥 下载语音文件:', voiceUrl);
|
|
|
|
const downloadResult = await new Promise((resolve, reject) => {
|
|
wx.downloadFile({
|
|
url: voiceUrl,
|
|
success: resolve,
|
|
fail: reject
|
|
});
|
|
});
|
|
|
|
if (downloadResult.statusCode === 200) {
|
|
// 缓存文件路径
|
|
this.voiceCache.set(voiceUrl, {
|
|
localPath: downloadResult.tempFilePath,
|
|
downloadTime: Date.now()
|
|
});
|
|
|
|
console.log('✅ 语音文件下载成功:', downloadResult.tempFilePath);
|
|
return downloadResult.tempFilePath;
|
|
} else {
|
|
throw new Error(`下载失败,状态码: ${downloadResult.statusCode}`);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('❌ 下载语音文件失败:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// 检查文件是否存在
|
|
isFileExists(filePath) {
|
|
try {
|
|
const fileManager = wx.getFileSystemManager();
|
|
const stats = fileManager.statSync(filePath);
|
|
return stats.isFile();
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 🔐 ===== 权限管理 =====
|
|
|
|
// 检查录音权限
|
|
async checkRecordPermission() {
|
|
try {
|
|
const setting = await new Promise((resolve, reject) => {
|
|
wx.getSetting({
|
|
success: resolve,
|
|
fail: reject
|
|
});
|
|
});
|
|
|
|
this.permissionGranted = setting.authSetting['scope.record'] === true;
|
|
console.log('🔐 录音权限状态:', this.permissionGranted);
|
|
|
|
return this.permissionGranted;
|
|
|
|
} catch (error) {
|
|
console.error('❌ 检查录音权限失败:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 请求录音权限
|
|
async requestRecordPermission() {
|
|
try {
|
|
await new Promise((resolve, reject) => {
|
|
wx.authorize({
|
|
scope: 'scope.record',
|
|
success: resolve,
|
|
fail: reject
|
|
});
|
|
});
|
|
|
|
this.permissionGranted = true;
|
|
console.log('✅ 录音权限获取成功');
|
|
return true;
|
|
|
|
} catch (error) {
|
|
console.error('❌ 录音权限获取失败:', error);
|
|
this.permissionGranted = false;
|
|
|
|
// 引导用户到设置页面
|
|
this.showPermissionGuide();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 显示权限引导
|
|
showPermissionGuide() {
|
|
wx.showModal({
|
|
title: '需要录音权限',
|
|
content: '使用语音消息功能需要录音权限,请在设置中开启',
|
|
confirmText: '去设置',
|
|
cancelText: '取消',
|
|
success: (res) => {
|
|
if (res.confirm) {
|
|
wx.openSetting({
|
|
success: (settingRes) => {
|
|
if (settingRes.authSetting['scope.record']) {
|
|
this.permissionGranted = true;
|
|
console.log('✅ 用户已开启录音权限');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// 📊 ===== 状态管理 =====
|
|
|
|
// 获取录音状态
|
|
getRecordingState() {
|
|
return { ...this.recordingState };
|
|
}
|
|
|
|
// 获取播放状态
|
|
getPlayingState() {
|
|
return { ...this.playingState };
|
|
}
|
|
|
|
// 是否正在录音
|
|
isRecording() {
|
|
return this.recordingState.isRecording;
|
|
}
|
|
|
|
// 是否正在播放
|
|
isPlaying() {
|
|
return this.playingState.isPlaying;
|
|
}
|
|
|
|
// 获取当前播放的消息ID
|
|
getCurrentPlayingMessageId() {
|
|
return this.playingState.playingMessageId;
|
|
}
|
|
|
|
// 🎧 ===== 事件管理 =====
|
|
|
|
// 注册事件监听器
|
|
on(event, callback) {
|
|
if (!this.eventListeners.has(event)) {
|
|
this.eventListeners.set(event, []);
|
|
}
|
|
this.eventListeners.get(event).push(callback);
|
|
}
|
|
|
|
// 移除事件监听器
|
|
off(event, callback) {
|
|
if (this.eventListeners.has(event)) {
|
|
const listeners = this.eventListeners.get(event);
|
|
const index = listeners.indexOf(callback);
|
|
if (index > -1) {
|
|
listeners.splice(index, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 触发事件
|
|
triggerEvent(event, data = null) {
|
|
if (this.eventListeners.has(event)) {
|
|
const listeners = this.eventListeners.get(event);
|
|
listeners.forEach(callback => {
|
|
try {
|
|
callback(data);
|
|
} catch (error) {
|
|
console.error(`❌ 事件处理器错误 [${event}]:`, error);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// 🔧 ===== 工具方法 =====
|
|
|
|
// 格式化时长
|
|
formatDuration(duration) {
|
|
const seconds = Math.floor(duration / 1000);
|
|
const minutes = Math.floor(seconds / 60);
|
|
const remainingSeconds = seconds % 60;
|
|
|
|
if (minutes > 0) {
|
|
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
|
|
} else {
|
|
return `${remainingSeconds}"`;
|
|
}
|
|
}
|
|
|
|
// 获取语音文件大小描述
|
|
getFileSizeDescription(fileSize) {
|
|
if (fileSize < 1024) {
|
|
return `${fileSize}B`;
|
|
} else if (fileSize < 1024 * 1024) {
|
|
return `${(fileSize / 1024).toFixed(1)}KB`;
|
|
} else {
|
|
return `${(fileSize / (1024 * 1024)).toFixed(1)}MB`;
|
|
}
|
|
}
|
|
|
|
// 清理缓存
|
|
clearCache() {
|
|
this.voiceCache.clear();
|
|
console.log('🧹 语音文件缓存已清理');
|
|
}
|
|
|
|
// 销毁管理器
|
|
destroy() {
|
|
// 停止录音和播放
|
|
if (this.recordingState.isRecording) {
|
|
this.cancelRecording();
|
|
}
|
|
|
|
if (this.playingState.isPlaying) {
|
|
this.stopPlaying();
|
|
}
|
|
|
|
// 销毁音频上下文
|
|
if (this.innerAudioContext) {
|
|
this.innerAudioContext.destroy();
|
|
this.innerAudioContext = null;
|
|
}
|
|
|
|
// 清理缓存和事件监听器
|
|
this.clearCache();
|
|
this.eventListeners.clear();
|
|
|
|
this.isInitialized = false;
|
|
console.log('🎤 语音消息管理器已销毁');
|
|
}
|
|
}
|
|
|
|
// 创建全局实例
|
|
const voiceMessageManager = new VoiceMessageManager();
|
|
|
|
module.exports = voiceMessageManager;
|