x
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* 支持安卓、iOS、H5的pcm格式,
|
||||
* 如果想支持mp3,需要增加mp3相关解码合并处理等,个人测试效果不太好,所以没有加
|
||||
*/
|
||||
export class StreamPlayer {
|
||||
constructor({
|
||||
inputSampleRate = 16000,
|
||||
numChannels = 1,
|
||||
bitDepth = 16,
|
||||
littleEndian = true,
|
||||
pcmType = 'int',
|
||||
callback = ()=>{}
|
||||
} = {}) {
|
||||
// 参数校验
|
||||
if (![16, 32].includes(bitDepth)) throw new Error('bitDepth 必须是 16 或 32');
|
||||
if (inputSampleRate <= 0) throw new Error('采样率必须大于 0');
|
||||
if (!['int', 'float'].includes(pcmType)) throw new Error('pcmType 必须是 int 或 float');
|
||||
|
||||
this.audioContext = new(window.AudioContext || window.webkitAudioContext)();
|
||||
this.inputSampleRate = inputSampleRate;
|
||||
this.numChannels = numChannels;
|
||||
this.bitDepth = bitDepth;
|
||||
this.littleEndian = littleEndian;
|
||||
this.pcmType = pcmType;
|
||||
this.audioQueue = [];
|
||||
this.playbackEndTime = 0;
|
||||
this.currentSource = null;
|
||||
this.lastBlockTail = null; // 用于交叉淡入淡出
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
appendChunk(audioData) {
|
||||
console.log('this.audioQueue', this.audioQueue.length);
|
||||
this.audioQueue.push(audioData);
|
||||
this._processQueue();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.audioQueue = [];
|
||||
this.playbackEndTime = 0;
|
||||
if (this.currentSource) {
|
||||
this.currentSource.stop();
|
||||
this.currentSource = null;
|
||||
}
|
||||
if (this.audioContext) {
|
||||
this.audioContext.close();
|
||||
this.audioContext = null;
|
||||
}
|
||||
}
|
||||
|
||||
async _processQueue() {
|
||||
if(this.audioQueue.length === 0) {
|
||||
this.callback('ended')
|
||||
return
|
||||
}
|
||||
if (!this.audioContext || this.currentSource) return;
|
||||
const buffer = this._mergeArrayBuffers(this.audioQueue)
|
||||
this.audioQueue = []
|
||||
try {
|
||||
const audioBuffer = this._convertPCM(buffer);
|
||||
await this._schedulePlay(audioBuffer);
|
||||
this._processQueue();
|
||||
} catch (err) {
|
||||
console.error('音频处理失败:', err.name, err.message, err.stack);
|
||||
}
|
||||
}
|
||||
|
||||
_convertPCM(buffer) {
|
||||
// 1. 数据对齐:确保长度是样本大小的整数倍
|
||||
const bytesPerSample = this.bitDepth / 8;
|
||||
const requiredBytes = bytesPerSample * this.numChannels;
|
||||
const validByteLength = Math.floor(buffer.byteLength / requiredBytes) * requiredBytes;
|
||||
const validBuffer = buffer.slice(0, validByteLength);
|
||||
const dataView = new DataView(validBuffer);
|
||||
const numSamples = validByteLength / requiredBytes;
|
||||
|
||||
// 2. 创建音频缓冲区
|
||||
const audioBuffer = this.audioContext.createBuffer(
|
||||
this.numChannels,
|
||||
numSamples,
|
||||
this.inputSampleRate
|
||||
);
|
||||
|
||||
// 3. 解析PCM数据
|
||||
for (let ch = 0; ch < this.numChannels; ch++) {
|
||||
const channelData = audioBuffer.getChannelData(ch);
|
||||
for (let i = 0; i < numSamples; i++) {
|
||||
// 计算字节位置(修正后的多声道索引)
|
||||
const pos = (i * this.numChannels * bytesPerSample) + (ch * bytesPerSample);
|
||||
let value;
|
||||
|
||||
// 根据位深和数据类型解析
|
||||
if (this.bitDepth === 16) {
|
||||
value = dataView.getInt16(pos, this.littleEndian) / 32768;
|
||||
} else {
|
||||
if (this.pcmType === 'int') {
|
||||
value = dataView.getInt32(pos, this.littleEndian) / 2147483648;
|
||||
} else {
|
||||
value = dataView.getFloat32(pos, this.littleEndian);
|
||||
}
|
||||
}
|
||||
channelData[i] = value;
|
||||
}
|
||||
|
||||
// 4. 消除直流偏移
|
||||
let sum = 0;
|
||||
for (let i = 0; i < channelData.length; i++) {
|
||||
sum += channelData[i];
|
||||
}
|
||||
const dcOffset = sum / channelData.length;
|
||||
for (let i = 0; i < channelData.length; i++) {
|
||||
channelData[i] -= dcOffset;
|
||||
}
|
||||
|
||||
// 5. 块内淡入淡出(减少边界突变)
|
||||
const fadeLength = Math.min(100, channelData.length);
|
||||
for (let i = 0; i < fadeLength; i++) {
|
||||
channelData[i] *= (i / fadeLength);
|
||||
}
|
||||
const startFadeOut = Math.max(0, channelData.length - fadeLength);
|
||||
for (let i = startFadeOut; i < channelData.length; i++) {
|
||||
const factor = 1 - (i - startFadeOut) / fadeLength;
|
||||
channelData[i] *= factor;
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 块间交叉淡入淡出(解决重音关键点)
|
||||
const crossfadeLength = 50;
|
||||
if (this.lastBlockTail && this.lastBlockTail.length === this.numChannels) {
|
||||
for (let ch = 0; ch < this.numChannels; ch++) {
|
||||
const channelData = audioBuffer.getChannelData(ch);
|
||||
const prevTail = this.lastBlockTail[ch];
|
||||
|
||||
// 只处理有足够长度交叉的情况
|
||||
if (prevTail.length >= crossfadeLength && channelData.length >= crossfadeLength) {
|
||||
for (let i = 0; i < crossfadeLength; i++) {
|
||||
const prevWeight = (crossfadeLength - i) / crossfadeLength;
|
||||
const currWeight = i / crossfadeLength;
|
||||
channelData[i] = channelData[i] * currWeight + prevTail[i] * prevWeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 保存当前块尾部用于下一次交叉
|
||||
this.lastBlockTail = [];
|
||||
for (let ch = 0; ch < this.numChannels; ch++) {
|
||||
const channelData = audioBuffer.getChannelData(ch);
|
||||
const tailStart = Math.max(0, channelData.length - crossfadeLength);
|
||||
this.lastBlockTail[ch] = channelData.slice(tailStart);
|
||||
}
|
||||
|
||||
return audioBuffer;
|
||||
}
|
||||
_schedulePlay(audioBuffer) {
|
||||
return new Promise(resolve => {
|
||||
const source = this.audioContext.createBufferSource();
|
||||
source.buffer = audioBuffer;
|
||||
source.connect(this.audioContext.destination);
|
||||
|
||||
const plannedStartTime = this.playbackEndTime;
|
||||
const now = this.audioContext.currentTime;
|
||||
let startTime = Math.max(plannedStartTime, now);
|
||||
|
||||
// 动态速率调整(示例,需根据实际测试调整阈值)
|
||||
const timeDrift = plannedStartTime - now;
|
||||
if (timeDrift < -0.02) {
|
||||
source.playbackRate.value = Math.min(1.04, 1 - (timeDrift / audioBuffer.duration));
|
||||
} else if (timeDrift > 0.02) {
|
||||
source.playbackRate.value = Math.max(0.96, 1 - (timeDrift / audioBuffer.duration));
|
||||
} else {
|
||||
source.playbackRate.value = 1.0;
|
||||
}
|
||||
|
||||
source.start(startTime);
|
||||
this.playbackEndTime = startTime + (audioBuffer.duration / source.playbackRate.value);
|
||||
|
||||
source.onended = () => {
|
||||
this.currentSource = null;
|
||||
resolve();
|
||||
};
|
||||
this.currentSource = source;
|
||||
});
|
||||
}
|
||||
|
||||
// 合并多个 ArrayBuffer 的辅助函数
|
||||
_mergeArrayBuffers(buffers) {
|
||||
let totalLength = 0;
|
||||
for (let buffer of buffers) {
|
||||
totalLength += buffer.byteLength;
|
||||
}
|
||||
const result = new Uint8Array(totalLength);
|
||||
let offset = 0;
|
||||
for (let buffer of buffers) {
|
||||
const view = new Uint8Array(buffer);
|
||||
result.set(view, offset);
|
||||
offset += view.length;
|
||||
}
|
||||
return result.buffer;
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,23 @@
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@@ -19,12 +31,14 @@
|
||||
SerializationType,
|
||||
CompressionType,
|
||||
ControlCommand,
|
||||
ProtocolConst,
|
||||
} from './ProtocolCodec';
|
||||
ProtocolConst
|
||||
} from './ProtocolCodec';
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
status: "未录音",
|
||||
currentPCMChunk: null,
|
||||
isStop: false, // 停止播放指令
|
||||
status: '未录音',
|
||||
currentDecibels: 0,
|
||||
isRecording: false,
|
||||
ws: null, // WebSocket 实例
|
||||
@@ -32,9 +46,10 @@
|
||||
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://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() {
|
||||
@@ -53,7 +68,7 @@
|
||||
},
|
||||
fail: () => {
|
||||
console.log('fail');
|
||||
},
|
||||
}
|
||||
});
|
||||
this.ws.onOpen((res) => {
|
||||
console.log('WebSocket连接已打开', res);
|
||||
@@ -66,39 +81,41 @@
|
||||
});
|
||||
});
|
||||
this.ws.onMessage((res) => {
|
||||
const {
|
||||
msgType,
|
||||
body
|
||||
} = ProtocolCodec.unpack(res.data)
|
||||
|
||||
const { msgType, body } = ProtocolCodec.unpack(res.data);
|
||||
|
||||
if (msgType === MessageType.IDENTITY) {
|
||||
console.log('身份校验成功', body);
|
||||
this.onStartRecord()
|
||||
this.onStartRecord();
|
||||
} else if (msgType === MessageType.AUDIO_DATA) {
|
||||
this.$refs.aaa.appendBuffer(body)
|
||||
}else {
|
||||
// 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"
|
||||
scope: 'scope.record'
|
||||
});
|
||||
const isGranted = res[0].grantStatus === 1;
|
||||
if (!isGranted) {
|
||||
uni.showToast({
|
||||
title: "请授予录音权限",
|
||||
icon: "none"
|
||||
title: '请授予录音权限',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
return isGranted;
|
||||
} catch (e) {
|
||||
console.error("申请权限失败:", e);
|
||||
console.error('申请权限失败:', e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
@@ -124,25 +141,22 @@
|
||||
sampleRate: 16000,
|
||||
frameSize: 1024,
|
||||
gain: 1.0,
|
||||
onFrameRecorded: ({
|
||||
isLastFrame,
|
||||
frameBuffer
|
||||
}) => {
|
||||
try{
|
||||
onFrameRecorded: ({ isLastFrame, frameBuffer }) => {
|
||||
try {
|
||||
this.ws.send({
|
||||
data: ProtocolCodec.pack(MessageType.AUDIO_DATA, frameBuffer)
|
||||
});
|
||||
} catch(e){}
|
||||
} catch (e) {}
|
||||
},
|
||||
onDecibels: (decibels) => {
|
||||
this.currentDecibels = decibels;
|
||||
}
|
||||
});
|
||||
this.isRecording = true;
|
||||
this.status = "录音中...";
|
||||
this.status = '录音中...';
|
||||
} catch (e) {
|
||||
console.error("启动录音失败:", e);
|
||||
this.status = "启动录音失败";
|
||||
console.error('启动录音失败:', e);
|
||||
this.status = '启动录音失败';
|
||||
this.isRecording = false;
|
||||
}
|
||||
},
|
||||
@@ -158,7 +172,7 @@
|
||||
// 停止录音组件
|
||||
this.$refs.recordFrame.stop();
|
||||
this.isRecording = false;
|
||||
this.status = "已停止录音";
|
||||
this.status = '已停止录音';
|
||||
}
|
||||
|
||||
// 关闭 WebSocket
|
||||
@@ -180,10 +194,7 @@
|
||||
},
|
||||
|
||||
// 接收音频帧并处理
|
||||
frameRecorded({
|
||||
isLastFrame,
|
||||
frameBuffer
|
||||
}) {
|
||||
frameRecorded({ isLastFrame, frameBuffer }) {
|
||||
// console.log("收到音频帧:", isLastFrame, frameBuffer.length);
|
||||
|
||||
// 2. 通过 WebSocket 发送给后端
|
||||
@@ -193,11 +204,9 @@
|
||||
data: frameBuffer
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("发送音频帧失败:", e);
|
||||
console.error('发送音频帧失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
|
||||
// 监听分贝值
|
||||
@@ -209,9 +218,62 @@
|
||||
// 录音停止回调
|
||||
stopIt(base64) {
|
||||
this.stopRecordAndClean();
|
||||
console.log("录音停止,最终音频Base64:", base64?.substring(0, 50) + "...");
|
||||
},
|
||||
},
|
||||
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>
|
||||
|
||||
@@ -238,4 +300,4 @@
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
-9
@@ -70,14 +70,6 @@
|
||||
}
|
||||
</script>
|
||||
<script module="renderJS" lang="renderjs">
|
||||
/**
|
||||
* 如果不需要支持iOS,建议使用这个,同时支持mp3和pcm
|
||||
*/
|
||||
// import { StreamPlayer } from "../../plugins/H5AndAndroidStreamPlayer";
|
||||
/**
|
||||
* 如果需要支持安卓iOS和H5,使用这个,支持pcm,暂时不支持mp3
|
||||
* 如果想支持mp3可以自己开发,我在测试中mp3的效果太差所以放弃了。
|
||||
*/
|
||||
import { StreamPlayer } from "../../plugins/StreamPlayer";
|
||||
let player=null
|
||||
export default {
|
||||
@@ -87,7 +79,6 @@
|
||||
},
|
||||
methods: {
|
||||
playTTS (base64) {
|
||||
console.log('base64', base64);
|
||||
if(!base64)return
|
||||
const binaryStr = atob(base64)
|
||||
const bytes = new Uint8Array(binaryStr.length)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"hash": "a4207d61",
|
||||
"configHash": "c22f3258",
|
||||
"lockfileHash": "86a59871",
|
||||
"browserHash": "52f5bffd",
|
||||
"hash": "1d959fb8",
|
||||
"configHash": "0d2436b6",
|
||||
"lockfileHash": "6ccee1ba",
|
||||
"browserHash": "2ee35306",
|
||||
"optimized": {
|
||||
"text-encoding": {
|
||||
"src": "../../../../../node_modules/text-encoding/index.js",
|
||||
"file": "text-encoding.js",
|
||||
"fileHash": "561a7cb8",
|
||||
"fileHash": "ad7e1515",
|
||||
"needsInterop": true
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,9 +3,9 @@ var __commonJS = (cb, mod) => function __require() {
|
||||
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
||||
};
|
||||
|
||||
// F:/aistream-test/测试流式传输uniapp/node_modules/text-encoding/lib/encoding-indexes.js
|
||||
// C:/Users/1/Desktop/testAudio/测试流式传输uniapp/node_modules/text-encoding/lib/encoding-indexes.js
|
||||
var require_encoding_indexes = __commonJS({
|
||||
"F:/aistream-test/测试流式传输uniapp/node_modules/text-encoding/lib/encoding-indexes.js"(exports, module) {
|
||||
"C:/Users/1/Desktop/testAudio/测试流式传输uniapp/node_modules/text-encoding/lib/encoding-indexes.js"(exports, module) {
|
||||
(function(global) {
|
||||
"use strict";
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
@@ -50,9 +50,9 @@ var require_encoding_indexes = __commonJS({
|
||||
}
|
||||
});
|
||||
|
||||
// F:/aistream-test/测试流式传输uniapp/node_modules/text-encoding/lib/encoding.js
|
||||
// C:/Users/1/Desktop/testAudio/测试流式传输uniapp/node_modules/text-encoding/lib/encoding.js
|
||||
var require_encoding = __commonJS({
|
||||
"F:/aistream-test/测试流式传输uniapp/node_modules/text-encoding/lib/encoding.js"(exports, module) {
|
||||
"C:/Users/1/Desktop/testAudio/测试流式传输uniapp/node_modules/text-encoding/lib/encoding.js"(exports, module) {
|
||||
(function(global) {
|
||||
"use strict";
|
||||
if (typeof module !== "undefined" && module.exports && !global["encoding-indexes"]) {
|
||||
@@ -1796,9 +1796,9 @@ var require_encoding = __commonJS({
|
||||
}
|
||||
});
|
||||
|
||||
// F:/aistream-test/测试流式传输uniapp/node_modules/text-encoding/index.js
|
||||
// C:/Users/1/Desktop/testAudio/测试流式传输uniapp/node_modules/text-encoding/index.js
|
||||
var require_text_encoding = __commonJS({
|
||||
"F:/aistream-test/测试流式传输uniapp/node_modules/text-encoding/index.js"(exports, module) {
|
||||
"C:/Users/1/Desktop/testAudio/测试流式传输uniapp/node_modules/text-encoding/index.js"(exports, module) {
|
||||
var encoding = require_encoding();
|
||||
module.exports = {
|
||||
TextEncoder: encoding.TextEncoder,
|
||||
|
||||
File diff suppressed because one or more lines are too long
+36
-15
@@ -1,6 +1,6 @@
|
||||
var __renderjsModules={};
|
||||
|
||||
__renderjsModules["5f91482f"] = (() => {
|
||||
__renderjsModules["49b84592"] = (() => {
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
@@ -45,7 +45,7 @@ __renderjsModules["5f91482f"] = (() => {
|
||||
default: () => stdin_default
|
||||
});
|
||||
|
||||
// C:/Users/1/Desktop/testAudio/测试流式传输uniapp/uni_modules/sdx-StreamPlayer/plugins/StreamPlayer.js
|
||||
// C:/Users/1/Desktop/testAudio/测试流式传输uniapp/pages/index/StreamPlayer.js
|
||||
var StreamPlayer = class {
|
||||
constructor({
|
||||
inputSampleRate = 16e3,
|
||||
@@ -106,7 +106,7 @@ __renderjsModules["5f91482f"] = (() => {
|
||||
yield this._schedulePlay(audioBuffer);
|
||||
this._processQueue();
|
||||
} catch (err) {
|
||||
console.error("\u97F3\u9891\u5904\u7406\u5931\u8D25:", err);
|
||||
console.error("\u97F3\u9891\u5904\u7406\u5931\u8D25:", err.name, err.message, err.stack);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -225,24 +225,45 @@ __renderjsModules["5f91482f"] = (() => {
|
||||
var stdin_default = {
|
||||
mounted() {
|
||||
const _this = this;
|
||||
player = new StreamPlayer({ callback: _this.callback });
|
||||
player = new StreamPlayer({
|
||||
inputSampleRate: 44100,
|
||||
// 后端PCM采样率(如8000/24000)
|
||||
numChannels: 1,
|
||||
// 声道数(单声道/双声道)
|
||||
bitDepth: 16,
|
||||
// 位深(16/32bit)
|
||||
littleEndian: true,
|
||||
// 端序(和后端一致)
|
||||
pcmType: "int",
|
||||
// 类型(int/float)
|
||||
callback: _this.callback
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
playTTS(base64) {
|
||||
console.log("base64", base64);
|
||||
if (!base64)
|
||||
appendPCMChunk(numArray) {
|
||||
if (!numArray || numArray.length === 0)
|
||||
return;
|
||||
const binaryStr = atob(base64);
|
||||
const bytes = new Uint8Array(binaryStr.length);
|
||||
for (let i = 0; i < binaryStr.length; i++) {
|
||||
bytes[i] = binaryStr.charCodeAt(i);
|
||||
}
|
||||
player.appendChunk(bytes.buffer);
|
||||
const uint8 = new Uint8Array(numArray);
|
||||
const arrayBuffer = uint8.buffer;
|
||||
player.appendChunk(arrayBuffer);
|
||||
},
|
||||
// 停止播放(销毁播放器)
|
||||
stop() {
|
||||
player && player.destroy();
|
||||
player = null;
|
||||
if (player) {
|
||||
player.destroy();
|
||||
player = null;
|
||||
const _this = this;
|
||||
player = new StreamPlayer({
|
||||
inputSampleRate: 16e3,
|
||||
numChannels: 1,
|
||||
bitDepth: 16,
|
||||
littleEndian: true,
|
||||
pcmType: "int",
|
||||
callback: _this.callback
|
||||
});
|
||||
}
|
||||
},
|
||||
// 播放结束回调(通知主线程)
|
||||
callback(e) {
|
||||
if (e === "ended") {
|
||||
this.$ownerInstance.callMethod("changeStreamPlaying", { type: false });
|
||||
|
||||
+48
-127
@@ -52,7 +52,7 @@ if (uni.restoreGlobal) {
|
||||
}
|
||||
return target;
|
||||
};
|
||||
const _sfc_main$3 = {
|
||||
const _sfc_main$2 = {
|
||||
data() {
|
||||
return {
|
||||
options: null,
|
||||
@@ -111,7 +111,7 @@ if (uni.restoreGlobal) {
|
||||
}
|
||||
}
|
||||
};
|
||||
function _sfc_render$2(_ctx, _cache, $props, $setup, $data, $options) {
|
||||
function _sfc_render$1(_ctx, _cache, $props, $setup, $data, $options) {
|
||||
return vue.openBlock(), vue.createElementBlock("view", {
|
||||
options: vue.wp($data.options),
|
||||
"change:options": _ctx.record.startRecord,
|
||||
@@ -120,91 +120,8 @@ if (uni.restoreGlobal) {
|
||||
}, null, 8, ["options", "change:options", "status", "change:status"]);
|
||||
}
|
||||
if (typeof block0$1 === "function")
|
||||
block0$1(_sfc_main$3);
|
||||
const __easycom_0 = /* @__PURE__ */ _export_sfc(_sfc_main$3, [["render", _sfc_render$2], ["__file", "C:/Users/1/Desktop/testAudio/测试流式传输uniapp/uni_modules/yao-RecordFrame/components/yao-RecordFrame/yao-RecordFrame.vue"]]);
|
||||
const block0 = (Comp) => {
|
||||
(Comp.$renderjs || (Comp.$renderjs = [])).push("renderJS");
|
||||
(Comp.$renderjsModules || (Comp.$renderjsModules = {}))["renderJS"] = "5f91482f";
|
||||
};
|
||||
const _sfc_main$2 = {
|
||||
data() {
|
||||
return {
|
||||
currBuffer: null,
|
||||
isStop: false,
|
||||
isStreamPlaying: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
connect() {
|
||||
this.ws = uni.connectSocket({
|
||||
url: "ws://172.16.89.58:8000/ws/audio",
|
||||
complete: () => {
|
||||
formatAppLog("log", "at uni_modules/sdx-StreamPlayer/components/sdx-StreamPlayer/sdx-StreamPlayer.vue:27", "complete");
|
||||
},
|
||||
success: () => {
|
||||
formatAppLog("log", "at uni_modules/sdx-StreamPlayer/components/sdx-StreamPlayer/sdx-StreamPlayer.vue:30", "web");
|
||||
this.ws.onOpen((res) => {
|
||||
formatAppLog("log", "at uni_modules/sdx-StreamPlayer/components/sdx-StreamPlayer/sdx-StreamPlayer.vue:32", "WebSocket连接已打开", res);
|
||||
this.ws.onMessage((res2) => {
|
||||
let messageData = res2.data;
|
||||
if (typeof messageData === "string") {
|
||||
try {
|
||||
const parsedData = JSON.parse(messageData);
|
||||
this.handleMessage(parsedData);
|
||||
} catch (e) {
|
||||
this.handleMessage(messageData);
|
||||
}
|
||||
} else if (messageData instanceof ArrayBuffer) {
|
||||
this.isStreamPlaying = true;
|
||||
this.currBuffer = uni.arrayBufferToBase64(messageData);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
end() {
|
||||
this.isStop = !this.isStop;
|
||||
},
|
||||
changeStreamPlaying(e) {
|
||||
this.isStreamPlaying = e.type;
|
||||
},
|
||||
appendBuffer(messageData) {
|
||||
this.isStreamPlaying = true;
|
||||
this.currBuffer = uni.arrayBufferToBase64(messageData);
|
||||
}
|
||||
}
|
||||
};
|
||||
function _sfc_render$1(_ctx, _cache, $props, $setup, $data, $options) {
|
||||
return vue.openBlock(), vue.createElementBlock(
|
||||
vue.Fragment,
|
||||
null,
|
||||
[
|
||||
vue.createElementVNode("button", {
|
||||
type: "default",
|
||||
onClick: _cache[0] || (_cache[0] = (...args) => $options.connect && $options.connect(...args))
|
||||
}, "播放音频"),
|
||||
vue.createElementVNode("button", {
|
||||
type: "default",
|
||||
onClick: _cache[1] || (_cache[1] = (...args) => $options.end && $options.end(...args))
|
||||
}, "结束播放"),
|
||||
vue.createCommentVNode(" 逻辑层和renderjs的交互需要通过这种方式传递数据,具体看文档 "),
|
||||
vue.createElementVNode("view", {
|
||||
isStop: vue.wp($data.isStop),
|
||||
"change:isStop": _ctx.renderJS.stop,
|
||||
prop: vue.wp($data.currBuffer),
|
||||
"change:prop": _ctx.renderJS.playTTS,
|
||||
type: "renderjs",
|
||||
module: "renderJS"
|
||||
}, null, 8, ["isStop", "change:isStop", "prop", "change:prop"])
|
||||
],
|
||||
64
|
||||
/* STABLE_FRAGMENT */
|
||||
);
|
||||
}
|
||||
if (typeof block0 === "function")
|
||||
block0(_sfc_main$2);
|
||||
const __easycom_1 = /* @__PURE__ */ _export_sfc(_sfc_main$2, [["render", _sfc_render$1], ["__file", "C:/Users/1/Desktop/testAudio/测试流式传输uniapp/uni_modules/sdx-StreamPlayer/components/sdx-StreamPlayer/sdx-StreamPlayer.vue"]]);
|
||||
block0$1(_sfc_main$2);
|
||||
const __easycom_0 = /* @__PURE__ */ _export_sfc(_sfc_main$2, [["render", _sfc_render$1], ["__file", "C:/Users/1/Desktop/testAudio/测试流式传输uniapp/uni_modules/yao-RecordFrame/components/yao-RecordFrame/yao-RecordFrame.vue"]]);
|
||||
var commonjsGlobal = typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : {};
|
||||
var encoding$1 = { exports: {} };
|
||||
var encodingIndexes = { exports: {} };
|
||||
@@ -2188,9 +2105,16 @@ if (uni.restoreGlobal) {
|
||||
};
|
||||
}
|
||||
}
|
||||
const block0 = (Comp) => {
|
||||
(Comp.$renderjs || (Comp.$renderjs = [])).push("renderJS");
|
||||
(Comp.$renderjsModules || (Comp.$renderjsModules = {}))["renderJS"] = "49b84592";
|
||||
};
|
||||
const _sfc_main$1 = {
|
||||
data() {
|
||||
return {
|
||||
currentPCMChunk: null,
|
||||
isStop: false,
|
||||
// 停止播放指令
|
||||
status: "未录音",
|
||||
currentDecibels: 0,
|
||||
isRecording: false,
|
||||
@@ -2205,9 +2129,10 @@ if (uni.restoreGlobal) {
|
||||
frameBufferList: [],
|
||||
// 缓存音频帧
|
||||
// wsUrl: "ws://127.0.0.1:8000/ws/audio", // 替换为实际后端地址
|
||||
wsUrl: "ws://172.16.89.58: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() {
|
||||
@@ -2218,17 +2143,17 @@ if (uni.restoreGlobal) {
|
||||
this.ws = uni.connectSocket({
|
||||
url: this.wsUrl,
|
||||
fail: () => {
|
||||
formatAppLog("log", "at pages/index/index.vue:49", "fail");
|
||||
formatAppLog("log", "at pages/index/index.vue:64", "fail");
|
||||
},
|
||||
success: () => {
|
||||
formatAppLog("log", "at pages/index/index.vue:52", "web");
|
||||
formatAppLog("log", "at pages/index/index.vue:67", "web");
|
||||
},
|
||||
fail: () => {
|
||||
formatAppLog("log", "at pages/index/index.vue:55", "fail");
|
||||
formatAppLog("log", "at pages/index/index.vue:70", "fail");
|
||||
}
|
||||
});
|
||||
this.ws.onOpen((res) => {
|
||||
formatAppLog("log", "at pages/index/index.vue:59", "WebSocket连接已打开", res);
|
||||
formatAppLog("log", "at pages/index/index.vue:74", "WebSocket连接已打开", res);
|
||||
this.ws.send({
|
||||
data: ProtocolCodec.pack(MessageType.IDENTITY, {
|
||||
user_id: "1001",
|
||||
@@ -2238,15 +2163,19 @@ if (uni.restoreGlobal) {
|
||||
});
|
||||
});
|
||||
this.ws.onMessage((res) => {
|
||||
const {
|
||||
msgType,
|
||||
body
|
||||
} = ProtocolCodec.unpack(res.data);
|
||||
const { msgType, body } = ProtocolCodec.unpack(res.data);
|
||||
if (msgType === MessageType.IDENTITY) {
|
||||
formatAppLog("log", "at pages/index/index.vue:75", "身份校验成功", body);
|
||||
formatAppLog("log", "at pages/index/index.vue:87", "身份校验成功", body);
|
||||
this.onStartRecord();
|
||||
} else if (msgType === MessageType.AUDIO_DATA) {
|
||||
this.$refs.aaa.appendBuffer(body);
|
||||
const buffer = body;
|
||||
if (buffer.byteLength < 2)
|
||||
return;
|
||||
const uint8 = new Uint8Array(buffer);
|
||||
const numArray = Array.from(uint8);
|
||||
this.currentPCMChunk = numArray;
|
||||
} else {
|
||||
formatAppLog("log", "at pages/index/index.vue:99", "其他消息", body);
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -2265,7 +2194,7 @@ if (uni.restoreGlobal) {
|
||||
}
|
||||
return isGranted;
|
||||
} catch (e) {
|
||||
formatAppLog("error", "at pages/index/index.vue:99", "申请权限失败:", e);
|
||||
formatAppLog("error", "at pages/index/index.vue:118", "申请权限失败:", e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
@@ -2281,16 +2210,13 @@ if (uni.restoreGlobal) {
|
||||
},
|
||||
// 开始录音
|
||||
onStartRecord() {
|
||||
formatAppLog("log", "at pages/index/index.vue:119", "开始录音");
|
||||
formatAppLog("log", "at pages/index/index.vue:138", "开始录音");
|
||||
try {
|
||||
this.$refs.recordFrame.start({
|
||||
sampleRate: 16e3,
|
||||
frameSize: 1024,
|
||||
gain: 1,
|
||||
onFrameRecorded: ({
|
||||
isLastFrame,
|
||||
frameBuffer
|
||||
}) => {
|
||||
onFrameRecorded: ({ isLastFrame, frameBuffer }) => {
|
||||
try {
|
||||
this.ws.send({
|
||||
data: ProtocolCodec.pack(MessageType.AUDIO_DATA, frameBuffer)
|
||||
@@ -2305,7 +2231,7 @@ if (uni.restoreGlobal) {
|
||||
this.isRecording = true;
|
||||
this.status = "录音中...";
|
||||
} catch (e) {
|
||||
formatAppLog("error", "at pages/index/index.vue:142", "启动录音失败:", e);
|
||||
formatAppLog("error", "at pages/index/index.vue:158", "启动录音失败:", e);
|
||||
this.status = "启动录音失败";
|
||||
this.isRecording = false;
|
||||
}
|
||||
@@ -2334,17 +2260,14 @@ if (uni.restoreGlobal) {
|
||||
this.currentDecibels = 0;
|
||||
},
|
||||
// 接收音频帧并处理
|
||||
frameRecorded({
|
||||
isLastFrame,
|
||||
frameBuffer
|
||||
}) {
|
||||
frameRecorded({ isLastFrame, frameBuffer }) {
|
||||
if (this.ws) {
|
||||
try {
|
||||
this.ws.send({
|
||||
data: frameBuffer
|
||||
});
|
||||
} catch (e) {
|
||||
formatAppLog("error", "at pages/index/index.vue:194", "发送音频帧失败:", e);
|
||||
formatAppLog("error", "at pages/index/index.vue:207", "发送音频帧失败:", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2354,13 +2277,12 @@ if (uni.restoreGlobal) {
|
||||
// 录音停止回调
|
||||
stopIt(base64) {
|
||||
this.stopRecordAndClean();
|
||||
formatAppLog("log", "at pages/index/index.vue:210", "录音停止,最终音频Base64:", (base64 == null ? void 0 : base64.substring(0, 50)) + "...");
|
||||
formatAppLog("log", "at pages/index/index.vue:221", "录音停止,最终音频Base64:", (base64 == null ? void 0 : base64.substring(0, 50)) + "...");
|
||||
}
|
||||
}
|
||||
};
|
||||
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
||||
const _component_yao_RecordFrame = resolveEasycom(vue.resolveDynamicComponent("yao-RecordFrame"), __easycom_0);
|
||||
const _component_sdx_StreamPlayer = resolveEasycom(vue.resolveDynamicComponent("sdx-StreamPlayer"), __easycom_1);
|
||||
return vue.openBlock(), vue.createElementBlock("view", { class: "container" }, [
|
||||
vue.createElementVNode("button", {
|
||||
onClick: _cache[0] || (_cache[0] = (...args) => $options.onStartRecord && $options.onStartRecord(...args)),
|
||||
@@ -2390,24 +2312,23 @@ if (uni.restoreGlobal) {
|
||||
onCurrentDecibels: $options.onCurrentDecibels,
|
||||
onOnStop: $options.stopIt
|
||||
}, null, 8, ["onOnFrameRecorded", "onCurrentDecibels", "onOnStop"]),
|
||||
vue.createVNode(
|
||||
_component_sdx_StreamPlayer,
|
||||
{ ref: "aaa" },
|
||||
{
|
||||
default: vue.withCtx(() => [
|
||||
vue.createTextVNode("xx")
|
||||
]),
|
||||
_: 1
|
||||
/* STABLE */
|
||||
},
|
||||
512
|
||||
/* NEED_PATCH */
|
||||
),
|
||||
vue.createCommentVNode(' <sdx-StreamPlayer ref="aaa">xx</sdx-StreamPlayer> '),
|
||||
vue.createElementVNode("button", {
|
||||
onClick: _cache[2] || (_cache[2] = (...args) => $options.test && $options.test(...args))
|
||||
}, "接通电话")
|
||||
}, "接通电话"),
|
||||
vue.createElementVNode("view", {
|
||||
ref: "renderJSModule",
|
||||
pcmChunk: vue.wp($data.currentPCMChunk),
|
||||
"change:pcmChunk": _ctx.renderJS.appendPCMChunk,
|
||||
isStop: vue.wp($data.isStop),
|
||||
"change:isStop": _ctx.renderJS.stop,
|
||||
type: "renderjs",
|
||||
module: "renderJS"
|
||||
}, null, 8, ["pcmChunk", "change:pcmChunk", "isStop", "change:isStop"])
|
||||
]);
|
||||
}
|
||||
if (typeof block0 === "function")
|
||||
block0(_sfc_main$1);
|
||||
const PagesIndexIndex = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["render", _sfc_render], ["__scopeId", "data-v-1cf27b2a"], ["__file", "C:/Users/1/Desktop/testAudio/测试流式传输uniapp/pages/index/index.vue"]]);
|
||||
__definePage("pages/index/index", PagesIndexIndex);
|
||||
const _sfc_main = {
|
||||
|
||||
Reference in New Issue
Block a user