Files
qingdao-employment-service/hook/useRealtimeRecorder2.js

348 lines
9.7 KiB
JavaScript
Raw Normal View History

2025-12-06 14:18:33 +08:00
import {
ref,
onUnmounted
} from 'vue'
import {
$api
} from '../common/globalFunction'; // 你的请求封装
import config from '@/config'
// 开源
export function useAudioRecorder() {
// --- 状态定义 ---
const isRecording = ref(false)
const isSocketConnected = ref(false)
const recordingDuration = ref(0)
const volumeLevel = ref(0) // 0-100
const recognizedText = ref('')
// --- 内部变量 ---
let socketTask = null
let durationTimer = null
// --- APP/小程序 变量 ---
let recorderManager = null;
// --- H5 变量 ---
let audioContext = null;
let scriptProcessor = null;
let mediaStreamSource = null;
let h5Stream = null;
// --- 配置项 ---
const RECORD_CONFIG = {
duration: 600000,
sampleRate: 16000,
numberOfChannels: 1,
format: 'pcm',
frameSize: 4096
}
/**
* 获取 WebSocket 地址 ( Token)
*/
const getWsUrl = async () => {
let wsUrl = config.vioceBaseURl
// 拼接 Token
const token = uni.getStorageSync('token') || '';
if (token) {
const separator = wsUrl.includes('?') ? '&' : '?';
wsUrl = `${wsUrl}${separator}token=${encodeURIComponent(token)}`;
}
return wsUrl;
}
/**
* 开始录音 (入口)
*/
const startRecording = async () => {
if (isRecording.value) return
try {
recognizedText.value = ''
volumeLevel.value = 0
// #ifdef H5
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
uni.showToast({
title: 'H5录音需要HTTPS环境',
icon: 'none'
});
return;
}
// #endif
const url = await getWsUrl()
console.log('正在连接 ASR:', url)
await connectSocket(url);
} catch (err) {
console.error('启动失败:', err);
uni.showToast({
title: '启动失败: ' + (err.message || ''),
icon: 'none'
});
cleanup();
}
}
/**
* 连接 WebSocket
*/
const connectSocket = (url) => {
return new Promise((resolve, reject) => {
socketTask = uni.connectSocket({
url: url,
success: () => console.log('Socket 连接请求发送'),
fail: (err) => reject(err)
});
socketTask.onOpen((res) => {
console.log('WebSocket 已连接');
isSocketConnected.value = true;
// #ifdef H5
startH5Recording().then(() => resolve()).catch(err => {
socketTask.close();
reject(err);
});
// #endif
// #ifndef H5
startAppRecording();
resolve();
// #endif
});
socketTask.onMessage((res) => {
// 接收文本结果
if (res.data) {
recognizedText.value = res.data;
}
});
socketTask.onError((err) => {
console.error('Socket 错误:', err);
isSocketConnected.value = false;
stopRecording();
});
socketTask.onClose(() => {
isSocketConnected.value = false;
console.log('Socket 已关闭');
});
})
}
const startH5Recording = async () => {
try {
// 1. 获取麦克风流
const stream = await navigator.mediaDevices.getUserMedia({
audio: true
});
h5Stream = stream;
// 2. 创建 AudioContext
const AudioContext = window.AudioContext || window.webkitAudioContext;
audioContext = new AudioContext({
sampleRate: 16000
});
mediaStreamSource = audioContext.createMediaStreamSource(stream);
scriptProcessor = audioContext.createScriptProcessor(4096, 1, 1);
scriptProcessor.onaudioprocess = (event) => {
if (!isSocketConnected.value || !socketTask) return;
const inputData = event.inputBuffer.getChannelData(0);
calculateVolume(inputData, true);
const buffer = new ArrayBuffer(inputData.length * 2);
const view = new DataView(buffer);
for (let i = 0; i < inputData.length; i++) {
let s = Math.max(-1, Math.min(1, inputData[i]));
view.setInt16(i * 2, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
}
socketTask.send({
data: buffer,
fail: (e) => console.error('发送音频失败', e)
});
};
mediaStreamSource.connect(scriptProcessor);
scriptProcessor.connect(audioContext.destination);
isRecording.value = true;
recordingDuration.value = 0;
durationTimer = setInterval(() => recordingDuration.value++, 1000);
console.log('H5 录音已启动');
} catch (err) {
console.error('H5 录音启动失败:', err);
throw err;
}
}
const stopH5Resources = () => {
if (scriptProcessor) scriptProcessor.disconnect();
if (mediaStreamSource) mediaStreamSource.disconnect();
if (audioContext) audioContext.close();
if (h5Stream) h5Stream.getTracks().forEach(track => track.stop());
scriptProcessor = null;
mediaStreamSource = null;
audioContext = null;
h5Stream = null;
}
const startAppRecording = () => {
recorderManager = uni.getRecorderManager();
recorderManager.onFrameRecorded((res) => {
const {
frameBuffer
} = res;
calculateVolume(frameBuffer, false);
if (isSocketConnected.value && socketTask) {
socketTask.send({
data: frameBuffer
});
}
});
recorderManager.onStart(() => {
console.log('APP 录音已开始');
isRecording.value = true;
recordingDuration.value = 0;
durationTimer = setInterval(() => recordingDuration.value++, 1000);
});
recorderManager.onError((err) => {
console.error('APP 录音报错:', err);
cleanup();
});
recorderManager.start(RECORD_CONFIG);
}
const stopHardwareResource = () => {
// APP/小程序停止
if (recorderManager) {
recorderManager.stop();
}
// H5停止
// #ifdef H5
if (scriptProcessor) scriptProcessor.disconnect();
if (mediaStreamSource) mediaStreamSource.disconnect();
if (audioContext) audioContext.close();
if (h5Stream) h5Stream.getTracks().forEach(track => track.stop());
scriptProcessor = null;
mediaStreamSource = null;
audioContext = null;
h5Stream = null;
// #endif
}
/**
* 停止录音 (通用)
*/
const stopRecording = () => {
// 停止 APP 录音
if (recorderManager) {
recorderManager.stop();
}
// 停止 H5 录音资源
// #ifdef H5
stopH5Resources();
// #endif
// 关闭 Socket
if (socketTask) {
socketTask.close();
}
cleanup();
}
const cancelRecording = () => {
if (!isRecording.value) return;
console.log('取消录音 - 丢弃结果');
// 1. 停止硬件录音
stopHardwareResource();
// 2. 强制关闭 Socket
if (socketTask) {
socketTask.close();
}
// 3. 关键:清空已识别的文本
recognizedText.value = '';
// 4. 清理资源
cleanup();
}
/**
* 清理状态
*/
const cleanup = () => {
clearInterval(durationTimer);
isRecording.value = false;
isSocketConnected.value = false;
socketTask = null;
recorderManager = null;
volumeLevel.value = 0;
}
/**
* 计算音量 (兼容 Float32 Int16/ArrayBuffer)
*/
const calculateVolume = (data, isFloat32) => {
let sum = 0;
let length = 0;
if (isFloat32) {
length = data.length;
for (let i = 0; i < length; i += 10) {
sum += Math.abs(data[i]);
}
volumeLevel.value = Math.min(100, Math.floor((sum / (length / 10)) * 100 * 3));
} else {
const int16Data = new Int16Array(data);
length = int16Data.length;
for (let i = 0; i < length; i += 10) {
sum += Math.abs(int16Data[i]);
}
const avg = sum / (length / 10);
volumeLevel.value = Math.min(100, Math.floor((avg / 10000) * 100));
}
}
onUnmounted(() => {
if (isRecording.value) {
stopRecording();
}
})
return {
isRecording,
isSocketConnected,
recordingDuration,
volumeLevel,
recognizedText,
startRecording,
stopRecording,
cancelRecording
}
}