517 lines
14 KiB
JavaScript
517 lines
14 KiB
JavaScript
/**
|
|
* 语音对讲工具类
|
|
* 支持录音、播放、权限管理等功能
|
|
* 适用于uniapp项目
|
|
*/
|
|
|
|
class VoiceIntercom {
|
|
constructor() {
|
|
this.recorderManager = null;
|
|
this.innerAudioContext = null;
|
|
this.isRecording = false;
|
|
this.isPlaying = false;
|
|
this.recordPath = '';
|
|
this.audioList = []; // 存储录音文件列表
|
|
this.maxRecordTime = 60000; // 最大录音时长60秒
|
|
this.recordOptions = {
|
|
duration: 60000,
|
|
sampleRate: 16000,
|
|
numberOfChannels: 1,
|
|
encodeBitRate: 96000,
|
|
format: 'mp3'
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 初始化录音管理器
|
|
*/
|
|
initRecorder() {
|
|
if (!this.recorderManager) {
|
|
this.recorderManager = uni.getRecorderManager();
|
|
this.setupRecorderEvents();
|
|
}
|
|
return this.recorderManager;
|
|
}
|
|
|
|
/**
|
|
* 初始化音频播放器
|
|
*/
|
|
initPlayer() {
|
|
if (!this.innerAudioContext) {
|
|
this.innerAudioContext = uni.createInnerAudioContext();
|
|
this.setupPlayerEvents();
|
|
}
|
|
return this.innerAudioContext;
|
|
}
|
|
|
|
/**
|
|
* 设置录音事件监听
|
|
*/
|
|
setupRecorderEvents() {
|
|
this.recorderManager.onStart(() => {
|
|
console.log('录音开始');
|
|
this.isRecording = true;
|
|
this.onRecordStart && this.onRecordStart();
|
|
});
|
|
|
|
this.recorderManager.onStop((res) => {
|
|
console.log('录音结束', res);
|
|
this.isRecording = false;
|
|
this.recordPath = res.tempFilePath;
|
|
this.audioList.push({
|
|
path: res.tempFilePath,
|
|
duration: res.duration,
|
|
fileSize: res.fileSize,
|
|
timestamp: Date.now()
|
|
});
|
|
this.onRecordStop && this.onRecordStop(res);
|
|
});
|
|
|
|
this.recorderManager.onError((err) => {
|
|
console.error('录音错误', err);
|
|
this.isRecording = false;
|
|
this.onRecordError && this.onRecordError(err);
|
|
});
|
|
|
|
this.recorderManager.onFrameRecorded((res) => {
|
|
// 实时录音数据,可用于实时传输
|
|
this.onFrameRecorded && this.onFrameRecorded(res);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 设置播放器事件监听
|
|
*/
|
|
setupPlayerEvents() {
|
|
this.innerAudioContext.onPlay(() => {
|
|
console.log('开始播放');
|
|
this.isPlaying = true;
|
|
this.onPlayStart && this.onPlayStart();
|
|
});
|
|
|
|
this.innerAudioContext.onEnded(() => {
|
|
console.log('播放结束');
|
|
this.isPlaying = false;
|
|
this.onPlayEnd && this.onPlayEnd();
|
|
});
|
|
|
|
this.innerAudioContext.onError((err) => {
|
|
console.error('播放错误', err);
|
|
this.isPlaying = false;
|
|
this.onPlayError && this.onPlayError(err);
|
|
});
|
|
|
|
this.innerAudioContext.onTimeUpdate(() => {
|
|
this.onTimeUpdate && this.onTimeUpdate({
|
|
currentTime: this.innerAudioContext.currentTime,
|
|
duration: this.innerAudioContext.duration
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 检查录音权限
|
|
*/
|
|
async checkRecordPermission() {
|
|
return new Promise((resolve, reject) => {
|
|
// #ifdef H5
|
|
this.checkH5RecordPermission().then(resolve).catch(reject)
|
|
// #endif
|
|
|
|
// #ifdef MP-WEIXIN || MP-ALIPAY || MP-BAIDU || MP-TOUTIAO
|
|
this.checkMiniProgramRecordPermission().then(resolve).catch(reject)
|
|
// #endif
|
|
|
|
// #ifdef APP-PLUS
|
|
this.checkAppRecordPermission().then(resolve).catch(reject)
|
|
// #endif
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 申请录音权限
|
|
*/
|
|
async requestRecordPermission() {
|
|
return new Promise((resolve, reject) => {
|
|
// #ifdef H5
|
|
this.requestH5RecordPermission().then(resolve).catch(reject)
|
|
// #endif
|
|
|
|
// #ifdef MP-WEIXIN || MP-ALIPAY || MP-BAIDU || MP-TOUTIAO
|
|
this.requestMiniProgramRecordPermission().then(resolve).catch(reject)
|
|
// #endif
|
|
|
|
// #ifdef APP-PLUS
|
|
this.requestAppRecordPermission().then(resolve).catch(reject)
|
|
// #endif
|
|
})
|
|
}
|
|
|
|
/**
|
|
* H5平台检查录音权限
|
|
*/
|
|
async checkH5RecordPermission() {
|
|
return new Promise((resolve, reject) => {
|
|
// #ifdef H5
|
|
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
|
navigator.mediaDevices.getUserMedia({ audio: true })
|
|
.then(() => {
|
|
resolve(true)
|
|
})
|
|
.catch((error) => {
|
|
console.error('H5录音权限检查失败:', error)
|
|
reject(error)
|
|
})
|
|
} else {
|
|
reject(new Error('浏览器不支持录音功能'))
|
|
}
|
|
// #endif
|
|
|
|
// #ifndef H5
|
|
resolve(true)
|
|
// #endif
|
|
})
|
|
}
|
|
|
|
/**
|
|
* H5平台申请录音权限
|
|
*/
|
|
async requestH5RecordPermission() {
|
|
return this.checkH5RecordPermission()
|
|
}
|
|
|
|
/**
|
|
* 小程序平台检查录音权限
|
|
*/
|
|
async checkMiniProgramRecordPermission() {
|
|
return new Promise((resolve, reject) => {
|
|
// #ifdef MP-WEIXIN || MP-ALIPAY || MP-BAIDU || MP-TOUTIAO
|
|
uni.getSetting({
|
|
success: (res) => {
|
|
if (res.authSetting['scope.record'] === true) {
|
|
resolve(true)
|
|
} else if (res.authSetting['scope.record'] === false) {
|
|
reject(new Error('用户拒绝了录音权限'))
|
|
} else {
|
|
// 未询问过权限
|
|
resolve(false)
|
|
}
|
|
},
|
|
fail: (err) => {
|
|
reject(err)
|
|
}
|
|
})
|
|
// #endif
|
|
|
|
// #ifndef MP-WEIXIN && !MP-ALIPAY && !MP-BAIDU && !MP-TOUTIAO
|
|
resolve(true)
|
|
// #endif
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 小程序平台申请录音权限
|
|
*/
|
|
async requestMiniProgramRecordPermission() {
|
|
return new Promise((resolve, reject) => {
|
|
// #ifdef MP-WEIXIN || MP-ALIPAY || MP-BAIDU || MP-TOUTIAO
|
|
uni.authorize({
|
|
scope: 'scope.record',
|
|
success: () => {
|
|
resolve(true)
|
|
},
|
|
fail: (err) => {
|
|
if (err.errMsg.includes('auth deny')) {
|
|
// 用户拒绝,引导到设置页面
|
|
this.showPermissionModal('录音权限', '需要录音权限才能使用语音对讲功能')
|
|
.then(() => {
|
|
uni.openSetting({
|
|
success: (res) => {
|
|
if (res.authSetting['scope.record']) {
|
|
resolve(true)
|
|
} else {
|
|
reject(new Error('用户拒绝录音权限'))
|
|
}
|
|
},
|
|
fail: reject
|
|
})
|
|
})
|
|
.catch(reject)
|
|
} else {
|
|
reject(err)
|
|
}
|
|
}
|
|
})
|
|
// #endif
|
|
|
|
// #ifndef MP-WEIXIN && !MP-ALIPAY && !MP-BAIDU && !MP-TOUTIAO
|
|
resolve(true)
|
|
// #endif
|
|
})
|
|
}
|
|
|
|
/**
|
|
* App平台检查录音权限
|
|
*/
|
|
async checkAppRecordPermission() {
|
|
return new Promise((resolve, reject) => {
|
|
// #ifdef APP-PLUS
|
|
plus.android.requestPermissions(
|
|
['android.permission.RECORD_AUDIO'],
|
|
(result) => {
|
|
const granted = result.granted && result.granted.length > 0
|
|
resolve(granted)
|
|
},
|
|
(error) => {
|
|
reject(error)
|
|
}
|
|
)
|
|
// #endif
|
|
|
|
// #ifndef APP-PLUS
|
|
resolve(true)
|
|
// #endif
|
|
})
|
|
}
|
|
|
|
/**
|
|
* App平台申请录音权限
|
|
*/
|
|
async requestAppRecordPermission() {
|
|
return this.checkAppRecordPermission()
|
|
}
|
|
|
|
/**
|
|
* 显示权限申请弹窗
|
|
*/
|
|
showPermissionModal(permissionName, description) {
|
|
return new Promise((resolve, reject) => {
|
|
uni.showModal({
|
|
title: '权限申请',
|
|
content: `需要${permissionName}才能使用相关功能。${description}`,
|
|
confirmText: '去设置',
|
|
cancelText: '取消',
|
|
success: (res) => {
|
|
if (res.confirm) {
|
|
resolve()
|
|
} else {
|
|
reject(new Error('用户取消权限申请'))
|
|
}
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 开始录音
|
|
*/
|
|
async startRecord(options = {}) {
|
|
try {
|
|
// 检查权限
|
|
const hasPermission = await this.checkRecordPermission();
|
|
|
|
// 如果没有权限,尝试申请
|
|
if (!hasPermission) {
|
|
await this.requestRecordPermission();
|
|
}
|
|
|
|
// 初始化录音管理器
|
|
this.initRecorder();
|
|
|
|
// 合并录音配置
|
|
const recordOptions = {...this.recordOptions, ...options };
|
|
|
|
// 开始录音
|
|
this.recorderManager.start(recordOptions);
|
|
|
|
return true;
|
|
} catch (error) {
|
|
console.error('开始录音失败', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 停止录音
|
|
*/
|
|
stopRecord() {
|
|
if (this.recorderManager && this.isRecording) {
|
|
this.recorderManager.stop();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 播放录音
|
|
*/
|
|
async playRecord(audioPath = null) {
|
|
try {
|
|
const path = audioPath || this.recordPath;
|
|
if (!path) {
|
|
throw new Error('没有可播放的录音文件');
|
|
}
|
|
|
|
// 初始化播放器
|
|
this.initPlayer();
|
|
|
|
// 停止当前播放
|
|
if (this.isPlaying) {
|
|
this.stopPlay();
|
|
}
|
|
|
|
// 设置音频源并播放
|
|
this.innerAudioContext.src = path;
|
|
this.innerAudioContext.play();
|
|
|
|
return true;
|
|
} catch (error) {
|
|
console.error('播放录音失败', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 停止播放
|
|
*/
|
|
stopPlay() {
|
|
if (this.innerAudioContext && this.isPlaying) {
|
|
this.innerAudioContext.stop();
|
|
this.isPlaying = false;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 暂停播放
|
|
*/
|
|
pausePlay() {
|
|
if (this.innerAudioContext && this.isPlaying) {
|
|
this.innerAudioContext.pause();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 恢复播放
|
|
*/
|
|
resumePlay() {
|
|
if (this.innerAudioContext && !this.isPlaying) {
|
|
this.innerAudioContext.play();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 设置播放进度
|
|
*/
|
|
seekPlay(position) {
|
|
if (this.innerAudioContext) {
|
|
this.innerAudioContext.seek(position);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 获取录音列表
|
|
*/
|
|
getAudioList() {
|
|
return this.audioList;
|
|
}
|
|
|
|
/**
|
|
* 删除录音文件
|
|
*/
|
|
deleteAudio(index) {
|
|
if (index >= 0 && index < this.audioList.length) {
|
|
const audio = this.audioList[index];
|
|
// 删除文件
|
|
uni.getFileSystemManager().unlink({
|
|
filePath: audio.path,
|
|
success: () => {
|
|
console.log('文件删除成功');
|
|
},
|
|
fail: (err) => {
|
|
console.error('文件删除失败', err);
|
|
}
|
|
});
|
|
// 从列表中移除
|
|
this.audioList.splice(index, 1);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 清空所有录音
|
|
*/
|
|
clearAllAudio() {
|
|
this.audioList.forEach(audio => {
|
|
uni.getFileSystemManager().unlink({
|
|
filePath: audio.path,
|
|
fail: (err) => {
|
|
console.error('文件删除失败', err);
|
|
}
|
|
});
|
|
});
|
|
this.audioList = [];
|
|
this.recordPath = '';
|
|
}
|
|
|
|
/**
|
|
* 上传录音文件
|
|
*/
|
|
async uploadAudio(audioPath, uploadUrl, formData = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
uni.uploadFile({
|
|
url: uploadUrl,
|
|
filePath: audioPath,
|
|
name: 'audio',
|
|
formData: formData,
|
|
success: (res) => {
|
|
resolve(res);
|
|
},
|
|
fail: (err) => {
|
|
reject(err);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 获取录音状态
|
|
*/
|
|
getRecordStatus() {
|
|
return {
|
|
isRecording: this.isRecording,
|
|
isPlaying: this.isPlaying,
|
|
recordPath: this.recordPath,
|
|
audioCount: this.audioList.length
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 销毁实例
|
|
*/
|
|
destroy() {
|
|
if (this.recorderManager) {
|
|
this.recorderManager.stop();
|
|
this.recorderManager = null;
|
|
}
|
|
if (this.innerAudioContext) {
|
|
this.innerAudioContext.destroy();
|
|
this.innerAudioContext = null;
|
|
}
|
|
this.isRecording = false;
|
|
this.isPlaying = false;
|
|
}
|
|
}
|
|
|
|
// 创建单例实例
|
|
const voiceIntercom = new VoiceIntercom();
|
|
|
|
export default voiceIntercom; |