444 lines
11 KiB
JavaScript
444 lines
11 KiB
JavaScript
// H5录音管理插件
|
||
// 完全兼容 uni.getRecorderManager API
|
||
|
||
// 检测当前平台
|
||
function isH5Platform() {
|
||
// #ifdef H5
|
||
return true;
|
||
// #endif
|
||
return false;
|
||
}
|
||
|
||
// H5录音管理器实现
|
||
class H5RecorderManager {
|
||
constructor() {
|
||
this.mediaRecorder = null;
|
||
this.audioContext = null;
|
||
this.analyser = null;
|
||
this.audioChunks = [];
|
||
this.startTime = null;
|
||
this.isPaused = false;
|
||
this.isRecording = false;
|
||
// 触摸结束取消录音(自定义变量)
|
||
this.touchEndCancelFlag = false;
|
||
|
||
// 事件回调函数
|
||
this.callbacks = {
|
||
onStart: [],
|
||
onPause: [],
|
||
onResume: [],
|
||
onStop: [],
|
||
onError: [],
|
||
onFrameRecorded: [],
|
||
onInterruptionBegin: [],
|
||
onInterruptionEnd: [],
|
||
};
|
||
|
||
// 默认配置
|
||
this.defaultOptions = {
|
||
duration: 60000, // 录音时长,单位ms,最大值600000(10分钟)
|
||
sampleRate: 44100, // 采样率,有效值 8000/16000/44100
|
||
numberOfChannels: 1, // 录音通道数,有效值 1/2
|
||
encodeBitRate: 192000, // 编码码率,有效值见下表格
|
||
format: 'mp3', // 音频格式,有效值 aac/mp3/wav/PCM
|
||
frameSize: null, // 指定帧大小,单位 KB
|
||
};
|
||
}
|
||
|
||
// 开始录音
|
||
start(options = {}) {
|
||
if (this.isRecording) {
|
||
this._triggerError('录音已在进行中');
|
||
return;
|
||
}
|
||
|
||
const config = Object.assign({}, this.defaultOptions, options);
|
||
// 检查浏览器支持
|
||
if (
|
||
typeof navigator.mediaDevices === 'undefined' ||
|
||
!navigator.mediaDevices.getUserMedia
|
||
) {
|
||
// 这里做一个消息原因增加说明
|
||
const isHttpsProtocol =
|
||
(typeof window !== 'undefined' &&
|
||
window.location.protocol === 'https:') ||
|
||
false;
|
||
|
||
const msg = !isHttpsProtocol
|
||
? '需要在HTTPS模式下且同意浏览器录音授权时支持语音'
|
||
: '当前浏览器不支持录音功能';
|
||
|
||
this._triggerError(msg);
|
||
return;
|
||
}
|
||
|
||
// 请求麦克风权限并开始录音
|
||
navigator.mediaDevices
|
||
.getUserMedia({
|
||
audio: {
|
||
sampleRate: config.sampleRate,
|
||
channelCount: config.numberOfChannels,
|
||
},
|
||
})
|
||
.then(stream => {
|
||
console.log('获取麦克风权限成功: stream', stream);
|
||
if (this.touchEndCancelFlag) {
|
||
this.touchEndCancelFlag = false;
|
||
return;
|
||
}
|
||
this._initRecorder(stream, config);
|
||
})
|
||
.catch(error => {
|
||
console.error('获取麦克风权限失败:', error);
|
||
this._triggerError('获取麦克风权限失败: ' + error.message);
|
||
});
|
||
}
|
||
|
||
// 初始化录音器
|
||
_initRecorder(stream, config) {
|
||
try {
|
||
// 创建AudioContext用于音频处理
|
||
this.audioContext = new (window.AudioContext ||
|
||
window.webkitAudioContext)();
|
||
|
||
// 创建MediaRecorder
|
||
const mimeType = this._getMimeType(config.format);
|
||
|
||
if (!MediaRecorder.isTypeSupported(mimeType)) {
|
||
uni.showToast({
|
||
title: `不支持${config.format}格式,将使用默认格式`,
|
||
icon: 'none',
|
||
});
|
||
}
|
||
|
||
this.mediaRecorder = new MediaRecorder(stream, {
|
||
mimeType: MediaRecorder.isTypeSupported(mimeType)
|
||
? mimeType
|
||
: undefined,
|
||
audioBitsPerSecond: config.encodeBitRate,
|
||
});
|
||
console.log(stream, this.mediaRecorder.mimeType);
|
||
|
||
this.audioChunks = [];
|
||
this.startTime = Date.now();
|
||
this.isRecording = true;
|
||
this.isPaused = false;
|
||
|
||
// 设置事件监听
|
||
this.mediaRecorder.ondataavailable = event => {
|
||
if (event.data.size > 0) {
|
||
this.audioChunks.push(event.data);
|
||
|
||
// 触发帧录制回调
|
||
if (config.frameSize && this.callbacks.onFrameRecorded.length > 0) {
|
||
this._triggerFrameRecorded(event.data, false);
|
||
}
|
||
}
|
||
};
|
||
|
||
this.mediaRecorder.onstop = () => {
|
||
this._handleRecordStop(config);
|
||
};
|
||
|
||
this.mediaRecorder.onerror = event => {
|
||
this._triggerError('录音过程中出现错误: ' + event.error);
|
||
};
|
||
|
||
// 开始录音
|
||
if (config.frameSize) {
|
||
// 如果设置了frameSize,定时收集数据
|
||
this.mediaRecorder.start(config.frameSize * 1024); // frameSize is in KB
|
||
} else {
|
||
this.mediaRecorder.start();
|
||
}
|
||
|
||
// 设置录音时长限制
|
||
if (config.duration && config.duration > 0) {
|
||
setTimeout(() => {
|
||
if (this.isRecording && !this.isPaused) {
|
||
this.stop();
|
||
}
|
||
}, config.duration);
|
||
}
|
||
|
||
// 触发开始事件
|
||
this._triggerStart();
|
||
} catch (error) {
|
||
console.error('初始化录音器失败:', error);
|
||
this._triggerError('初始化录音器失败: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// 获取MIME类型
|
||
_getMimeType(format) {
|
||
const mimeTypes = {
|
||
mp3: 'audio/mpeg',
|
||
wav: 'audio/wav',
|
||
aac: 'audio/aac',
|
||
PCM: 'audio/wav',
|
||
};
|
||
return mimeTypes[format] || 'audio/webm';
|
||
}
|
||
|
||
// 暂停录音
|
||
pause() {
|
||
if (!this.isRecording || this.isPaused) {
|
||
return;
|
||
}
|
||
|
||
if (this.mediaRecorder && this.mediaRecorder.state === 'recording') {
|
||
this.mediaRecorder.pause();
|
||
this.isPaused = true;
|
||
this._triggerPause();
|
||
}
|
||
}
|
||
|
||
// 继续录音
|
||
resume() {
|
||
if (!this.isRecording || !this.isPaused) {
|
||
return;
|
||
}
|
||
|
||
if (this.mediaRecorder && this.mediaRecorder.state === 'paused') {
|
||
this.mediaRecorder.resume();
|
||
this.isPaused = false;
|
||
this._triggerResume();
|
||
}
|
||
}
|
||
|
||
// 停止录音
|
||
stop() {
|
||
if (!this.isRecording) {
|
||
return;
|
||
}
|
||
|
||
if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
|
||
this.mediaRecorder.stop();
|
||
}
|
||
|
||
// 停止所有音频轨道
|
||
if (this.mediaRecorder && this.mediaRecorder.stream) {
|
||
this.mediaRecorder.stream.getTracks().forEach(track => track.stop());
|
||
}
|
||
|
||
this.isRecording = false;
|
||
this.isPaused = false;
|
||
}
|
||
|
||
// 处理录音停止
|
||
_handleRecordStop(config) {
|
||
if (this.audioChunks.length === 0) {
|
||
this._triggerError('没有录制到音频数据');
|
||
return;
|
||
}
|
||
|
||
// 创建音频文件
|
||
const audioBlob = new Blob(this.audioChunks, {
|
||
type: this._getMimeType(config.format),
|
||
});
|
||
|
||
// 创建临时文件URL
|
||
const tempFilePath = URL.createObjectURL(audioBlob);
|
||
|
||
// 计算录音时长
|
||
const duration = this.startTime ? (Date.now() - this.startTime) / 1000 : 0;
|
||
|
||
// 触发停止事件
|
||
this._triggerStop({
|
||
tempFilePath: tempFilePath,
|
||
duration: duration,
|
||
fileSize: audioBlob.size,
|
||
});
|
||
|
||
// 清理资源
|
||
this._cleanup();
|
||
}
|
||
|
||
// 清理资源
|
||
_cleanup() {
|
||
if (this.audioContext) {
|
||
this.audioContext.close();
|
||
this.audioContext = null;
|
||
}
|
||
|
||
this.mediaRecorder = null;
|
||
this.audioChunks = [];
|
||
this.startTime = null;
|
||
this.touchEndCancelFlag = false;
|
||
}
|
||
|
||
// 事件监听方法
|
||
onStart(callback) {
|
||
if (typeof callback === 'function') {
|
||
this.callbacks.onStart.push(callback);
|
||
}
|
||
}
|
||
|
||
onPause(callback) {
|
||
if (typeof callback === 'function') {
|
||
this.callbacks.onPause.push(callback);
|
||
}
|
||
}
|
||
|
||
onResume(callback) {
|
||
if (typeof callback === 'function') {
|
||
this.callbacks.onResume.push(callback);
|
||
}
|
||
}
|
||
|
||
onStop(callback) {
|
||
if (typeof callback === 'function') {
|
||
this.callbacks.onStop.push(callback);
|
||
}
|
||
}
|
||
|
||
onError(callback) {
|
||
if (typeof callback === 'function') {
|
||
this.callbacks.onError.push(callback);
|
||
}
|
||
}
|
||
|
||
onFrameRecorded(callback) {
|
||
if (typeof callback === 'function') {
|
||
this.callbacks.onFrameRecorded.push(callback);
|
||
}
|
||
}
|
||
|
||
onInterruptionBegin(callback) {
|
||
if (typeof callback === 'function') {
|
||
this.callbacks.onInterruptionBegin.push(callback);
|
||
}
|
||
}
|
||
|
||
onInterruptionEnd(callback) {
|
||
if (typeof callback === 'function') {
|
||
this.callbacks.onInterruptionEnd.push(callback);
|
||
}
|
||
}
|
||
|
||
// 移除事件监听方法(支付宝小程序兼容)
|
||
offStart(callback) {
|
||
this._removeCallback('onStart', callback);
|
||
}
|
||
|
||
offPause(callback) {
|
||
this._removeCallback('onPause', callback);
|
||
}
|
||
|
||
offResume(callback) {
|
||
this._removeCallback('onResume', callback);
|
||
}
|
||
|
||
offStop(callback) {
|
||
this._removeCallback('onStop', callback);
|
||
}
|
||
|
||
offFrameRecorded(callback) {
|
||
this._removeCallback('onFrameRecorded', callback);
|
||
}
|
||
|
||
// 移除回调函数
|
||
_removeCallback(eventName, callback) {
|
||
if (callback && this.callbacks[eventName]) {
|
||
const index = this.callbacks[eventName].indexOf(callback);
|
||
if (index > -1) {
|
||
this.callbacks[eventName].splice(index, 1);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 触发事件的私有方法
|
||
_triggerStart() {
|
||
this.callbacks.onStart.forEach(callback => {
|
||
try {
|
||
callback();
|
||
} catch (error) {
|
||
console.error('onStart callback error:', error);
|
||
}
|
||
});
|
||
}
|
||
|
||
_triggerPause() {
|
||
this.callbacks.onPause.forEach(callback => {
|
||
try {
|
||
callback();
|
||
} catch (error) {
|
||
console.error('onPause callback error:', error);
|
||
}
|
||
});
|
||
}
|
||
|
||
_triggerResume() {
|
||
// 重置触摸结束取消录音标志
|
||
this.touchEndCancelFlag = false;
|
||
this.callbacks.onResume.forEach(callback => {
|
||
try {
|
||
callback();
|
||
} catch (error) {
|
||
console.error('onResume callback error:', error);
|
||
}
|
||
});
|
||
}
|
||
|
||
_triggerStop(result) {
|
||
// 重置触摸结束取消录音标志
|
||
this.touchEndCancelFlag = false;
|
||
this.callbacks.onStop.forEach(callback => {
|
||
try {
|
||
callback(result);
|
||
} catch (error) {
|
||
console.error('onStop callback error:', error);
|
||
}
|
||
});
|
||
}
|
||
|
||
_triggerError(errMsg) {
|
||
// 重置触摸结束取消录音标志
|
||
this.touchEndCancelFlag = false;
|
||
this.callbacks.onError.forEach(callback => {
|
||
try {
|
||
callback({ errMsg });
|
||
} catch (error) {
|
||
console.error('onError callback error:', error);
|
||
}
|
||
});
|
||
}
|
||
|
||
_triggerFrameRecorded(frameBuffer, isLastFrame = false) {
|
||
// 重置触摸结束取消录音标志
|
||
this.touchEndCancelFlag = false;
|
||
this.callbacks.onFrameRecorded.forEach(callback => {
|
||
try {
|
||
callback({
|
||
frameBuffer,
|
||
isLastFrame,
|
||
});
|
||
} catch (error) {
|
||
console.error('onFrameRecorded callback error:', error);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// 全局实例
|
||
let recorderManagerInstance = null;
|
||
|
||
// 主函数:获取录音管理器
|
||
function getRecorderManager() {
|
||
if (isH5Platform()) {
|
||
// H5平台使用自定义实现
|
||
if (!recorderManagerInstance) {
|
||
recorderManagerInstance = new H5RecorderManager();
|
||
}
|
||
return recorderManagerInstance;
|
||
} else {
|
||
// 非H5平台直接使用uni.getRecorderManager
|
||
return uni.getRecorderManager();
|
||
}
|
||
}
|
||
|
||
// 导出
|
||
export default {
|
||
getRecorderManager,
|
||
};
|