Files
aistream-test/测试流式传输uniapp/pages/index/index.vue
T
2025-12-04 17:42:00 +08:00

304 lines
7.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="container">
<button @click="onStartRecord" :disabled="isRecording">开始录音</button>
<button @click="onStopRecord" :disabled="!isRecording">停止录音</button>
<view class="tip">{{ status }}</view>
<view class="tip">当前分贝{{ currentDecibels }}</view>
<yao-RecordFrame
ref="recordFrame"
@onFrameRecorded="frameRecorded"
@currentDecibels="onCurrentDecibels"
@onStop="stopIt"
></yao-RecordFrame>
<!-- <sdx-StreamPlayer ref="aaa">xx</sdx-StreamPlayer> -->
<button @click="test">接通电话</button>
<view
ref="renderJSModule"
:pcmChunk="currentPCMChunk"
:change:pcmChunk="renderJS.appendPCMChunk"
:isStop="isStop"
:change:isStop="renderJS.stop"
type="renderjs"
module="renderJS"
></view>
</view>
</template>
<script>
import {
ProtocolCodec,
MessageType,
SerializationType,
CompressionType,
ControlCommand,
ProtocolConst
} from './ProtocolCodec';
export default {
data() {
return {
currentPCMChunk: null,
isStop: false, // 停止播放指令
status: '未录音',
currentDecibels: 0,
isRecording: false,
ws: null, // WebSocket 实例
audioContext: null, // 音频上下文
scriptProcessor: null, // 音频处理节点
audioBufferSource: null, // 音频源节点
frameBufferList: [], // 缓存音频帧
// wsUrl: "ws://127.0.0.1:8000/ws/audio", // 替换为实际后端地址
// wsUrl: 'ws://172.16.89.58:8000/ws/audio' // 替换为实际后端地址
// wsUrl: "ws://25.64.32.157:9603/trapractice/voiceCallSocket/voiceCall?summary=3D072C9D242C96EA3F0FD647CB14A6F4B4BEA4493BB2FAAB065935FB405071B8A903128E488B6D06BEB23FD4E3D15EE6", // 替换为实际后端地址
wsUrl: "wss://aitstest.jlbank.com.cn:7001/trapractice/voiceCallSocket/voiceCall?summary=3D072C9D242C96EA3F0FD647CB14A6F44A8F9EF1C3D9726612B7339B566489139850DF3BE002DF99495FD60B34A5899D", // 替换为实际后端地址
};
},
onUnload() {
// 页面卸载时清理资源
this.stopRecordAndClean();
},
methods: {
test() {
this.ws = uni.connectSocket({
url: this.wsUrl,
fail: () => {
console.log('fail');
},
success: () => {
console.log('web');
},
fail: () => {
console.log('fail');
}
});
this.ws.onOpen((res) => {
console.log('WebSocket连接已打开', res);
this.ws.send({
data: ProtocolCodec.pack(MessageType.IDENTITY, {
user_id: '1001',
token: 'your_auth_token',
name: '测试用户'
})
});
});
this.ws.onMessage((res) => {
const { msgType, body } = ProtocolCodec.unpack(res.data);
if (msgType === MessageType.IDENTITY) {
console.log('身份校验成功', body);
this.onStartRecord();
} else if (msgType === MessageType.AUDIO_DATA) {
// this.$refs.aaa.appendBuffer(body)
// this.currentPCMChunk = body;
const buffer = body;
if (buffer.byteLength < 2) return;
// ArrayBuffer转数字数组
const uint8 = new Uint8Array(buffer);
const numArray = Array.from(uint8);
this.currentPCMChunk = numArray; // 传递数组
} else {
console.log('其他消息', body);
}
});
},
// 申请录音权限
async applyRecordPermission() {
try {
const res = await uni.requestPermissions({
scope: 'scope.record'
});
const isGranted = res[0].grantStatus === 1;
if (!isGranted) {
uni.showToast({
title: '请授予录音权限',
icon: 'none'
});
}
return isGranted;
} catch (e) {
console.error('申请权限失败:', e);
return false;
}
},
// ArrayBuffer 转字符串
// 兼容的 ArrayBuffer 转字符串方法
arrayBufferToStringCompat(buffer) {
// 方法1: 使用 String.fromCharCode 和 Uint8Array
const uint8Array = new Uint8Array(buffer);
let str = '';
for (let i = 0; i < uint8Array.length; i++) {
str += String.fromCharCode(uint8Array[i]);
}
return str;
// 方法2: 或者使用更简洁的方式
// return String.fromCharCode.apply(null, new Uint8Array(buffer));
},
// 开始录音
onStartRecord() {
console.log('开始录音');
try {
this.$refs.recordFrame.start({
sampleRate: 16000,
frameSize: 1024,
gain: 1.0,
onFrameRecorded: ({ isLastFrame, frameBuffer }) => {
try {
this.ws.send({
data: ProtocolCodec.pack(MessageType.AUDIO_DATA, frameBuffer)
});
} catch (e) {}
},
onDecibels: (decibels) => {
this.currentDecibels = decibels;
}
});
this.isRecording = true;
this.status = '录音中...';
} catch (e) {
console.error('启动录音失败:', e);
this.status = '启动录音失败';
this.isRecording = false;
}
},
// 停止录音
onStopRecord() {
this.stopRecordAndClean();
},
// 停止录音并清理资源
stopRecordAndClean() {
if (this.isRecording) {
// 停止录音组件
this.$refs.recordFrame.stop();
this.isRecording = false;
this.status = '已停止录音';
}
// 关闭 WebSocket
if (this.ws) {
this.ws.close();
this.ws = null;
}
// 清理音频上下文
if (this.audioContext) {
this.audioContext.close();
this.audioContext = null;
this.scriptProcessor = null;
this.frameBufferList = [];
}
// 重置状态
this.currentDecibels = 0;
},
// 接收音频帧并处理
frameRecorded({ isLastFrame, frameBuffer }) {
// console.log("收到音频帧:", isLastFrame, frameBuffer.length);
// 2. 通过 WebSocket 发送给后端
if (this.ws) {
try {
this.ws.send({
data: frameBuffer
});
} catch (e) {
console.error('发送音频帧失败:', e);
}
}
},
// 监听分贝值
onCurrentDecibels(decibels) {
// this.currentDecibels = decibels.toFixed(2);
// console.log("当前分贝:", this.currentDecibels);
},
// 录音停止回调
stopIt(base64) {
this.stopRecordAndClean();
console.log('录音停止,最终音频Base64', base64?.substring(0, 50) + '...');
}
}
};
</script>
<script module="renderJS" lang="renderjs">
import { StreamPlayer } from ".//StreamPlayer";
let player = null;
export default {
mounted() {
const _this = this;
player = new StreamPlayer({
inputSampleRate: 44100, // 后端PCM采样率(如8000/24000
numChannels: 1, // 声道数(单声道/双声道)
bitDepth: 16, // 位深(16/32bit
littleEndian: true, // 端序(和后端一致)
pcmType: 'int', // 类型(int/float
callback: _this.callback
});
},
methods: {
appendPCMChunk(numArray) {
if (!numArray || numArray.length === 0) return;
// 数字数组转回ArrayBuffer
const uint8 = new Uint8Array(numArray);
const arrayBuffer = uint8.buffer;
player.appendChunk(arrayBuffer);
},
// 停止播放(销毁播放器)
stop() {
if (player) {
player.destroy();
player = null;
// 重新初始化(方便后续再次播放)
const _this = this;
player = new StreamPlayer({
inputSampleRate: 16000,
numChannels: 1,
bitDepth: 16,
littleEndian: true,
pcmType: 'int',
callback: _this.callback
});
}
},
// 播放结束回调(通知主线程)
callback(e) {
if (e === 'ended') {
this.$ownerInstance.callMethod('changeStreamPlaying', { type: false });
}
}
}
};
</script>
<style scoped>
.container {
padding: 20rpx;
}
button {
margin: 10rpx 0;
padding: 15rpx 30rpx;
background: #007aff;
color: white;
border: none;
border-radius: 8rpx;
}
button:disabled {
background: #ccc;
}
.tip {
margin: 15rpx 0;
font-size: 28rpx;
color: #333;
}
</style>