@easyv/biz-components 0.0.18 → 0.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
- import { XunFeiStreamVoiceManager, XunFeiStreamVoiceManagerConfig } from '../utils';
1
+ import { XFStreamVoiceManager, XFStreamVoiceManagerConfig } from '../utils';
2
2
 
3
3
  /**
4
4
  * 讯飞流式语音识别的 hook,注意 config 只在初识化时注册一次,后续更改,请使用 manager.setConfig 方法更新
5
5
  * @param config XunFeiStreamVoiceManagerConfig
6
6
  * @returns
7
7
  */
8
- export declare const useXunFeiSteamVoiceManager: (config: XunFeiStreamVoiceManagerConfig) => XunFeiStreamVoiceManager | null;
8
+ export declare const useXunFeiSteamVoiceManager: (config: XFStreamVoiceManagerConfig) => XFStreamVoiceManager | null;
@@ -1,10 +1,10 @@
1
- import { useState as o, useEffect as a } from "react";
2
- import { XunFeiStreamVoiceManager as s } from "../utils/xunFeiVoice/XunFeiStreamVoiceManager.es.js";
1
+ import { useState as n, useEffect as a } from "react";
2
+ import { XFStreamVoiceManager as s } from "../utils/xunFeiVoice/XFStreamVoiceManager.es.js";
3
3
  const c = (t) => {
4
- const [r, n] = o(null);
4
+ const [r, o] = n(null);
5
5
  return a(() => {
6
6
  const e = new s(t);
7
- return n(e), () => {
7
+ return o(e), () => {
8
8
  e == null || e.destroy();
9
9
  };
10
10
  }, []), r;
@@ -1 +1 @@
1
- {"version":3,"file":"useXunFeiSteamVoiceManager.es.js","sources":["../../src/hooks/useXunFeiSteamVoiceManager.ts"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport { XunFeiStreamVoiceManager, XunFeiStreamVoiceManagerConfig } from '@/utils';\n\n/**\n * 讯飞流式语音识别的 hook,注意 config 只在初识化时注册一次,后续更改,请使用 manager.setConfig 方法更新\n * @param config XunFeiStreamVoiceManagerConfig\n * @returns\n */\nexport const useXunFeiSteamVoiceManager = (config: XunFeiStreamVoiceManagerConfig) => {\n const [manager, setManager] = useState<XunFeiStreamVoiceManager | null>(null);\n\n useEffect(() => {\n const newManager = new XunFeiStreamVoiceManager(config);\n setManager(newManager);\n return () => {\n newManager?.destroy();\n };\n }, []);\n return manager;\n};\n"],"names":["useXunFeiSteamVoiceManager","config","manager","setManager","useState","useEffect","newManager","XunFeiStreamVoiceManager"],"mappings":";;AAQa,MAAAA,IAA6B,CAACC,MAA2C;AACpF,QAAM,CAACC,GAASC,CAAU,IAAIC,EAA0C,IAAI;AAE5E,SAAAC,EAAU,MAAM;AACR,UAAAC,IAAa,IAAIC,EAAyBN,CAAM;AACtD,WAAAE,EAAWG,CAAU,GACd,MAAM;AACX,MAAAA,KAAA,QAAAA,EAAY;AAAA,IACd;AAAA,EACF,GAAG,EAAE,GACEJ;AACT;"}
1
+ {"version":3,"file":"useXunFeiSteamVoiceManager.es.js","sources":["../../src/hooks/useXunFeiSteamVoiceManager.ts"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport { XFStreamVoiceManager, XFStreamVoiceManagerConfig } from '@/utils';\n\n/**\n * 讯飞流式语音识别的 hook,注意 config 只在初识化时注册一次,后续更改,请使用 manager.setConfig 方法更新\n * @param config XunFeiStreamVoiceManagerConfig\n * @returns\n */\nexport const useXunFeiSteamVoiceManager = (config: XFStreamVoiceManagerConfig) => {\n const [manager, setManager] = useState<XFStreamVoiceManager | null>(null);\n\n useEffect(() => {\n const newManager = new XFStreamVoiceManager(config);\n setManager(newManager);\n return () => {\n newManager?.destroy();\n };\n }, []);\n return manager;\n};\n"],"names":["useXunFeiSteamVoiceManager","config","manager","setManager","useState","useEffect","newManager","XFStreamVoiceManager"],"mappings":";;AAQa,MAAAA,IAA6B,CAACC,MAAuC;AAChF,QAAM,CAACC,GAASC,CAAU,IAAIC,EAAsC,IAAI;AAExE,SAAAC,EAAU,MAAM;AACR,UAAAC,IAAa,IAAIC,EAAqBN,CAAM;AAClD,WAAAE,EAAWG,CAAU,GACd,MAAM;AACX,MAAAA,KAAA,QAAAA,EAAY;AAAA,IACd;AAAA,EACF,GAAG,EAAE,GACEJ;AACT;"}
package/dist/index.es.js CHANGED
@@ -1,23 +1,23 @@
1
1
  /* empty css */
2
2
  import { AiMessageRender as t } from "./components/AiMessageRender/AiMessageRender.es.js";
3
- import { ShadowDom as n } from "./components/ShadowDom/ShadowDom.es.js";
4
- import { ScrollController as i } from "./components/ScrollController/ScrollController.es.js";
5
- import { VoiceAnimation as f } from "./components/VoiceAnimation/VoiceAnimation.es.js";
3
+ import { ShadowDom as a } from "./components/ShadowDom/ShadowDom.es.js";
4
+ import { ScrollController as p } from "./components/ScrollController/ScrollController.es.js";
5
+ import { VoiceAnimation as i } from "./components/VoiceAnimation/VoiceAnimation.es.js";
6
6
  import { useDivAutoScroll as c } from "./hooks/useDivAutoScroll.es.js";
7
- import { useXunFeiSteamVoiceManager as u } from "./hooks/useXunFeiSteamVoiceManager.es.js";
8
- import { FunASRManager as l, getWebSocketConnectForFunASR as A } from "./utils/funASR/funASRManager.es.js";
9
- import { XunFeiStreamVoiceManager as M } from "./utils/xunFeiVoice/XunFeiStreamVoiceManager.es.js";
7
+ import { useXunFeiSteamVoiceManager as g } from "./hooks/useXunFeiSteamVoiceManager.es.js";
8
+ import { FunASRManager as u, getWebSocketConnectForFunASR as A } from "./utils/funASR/funASRManager.es.js";
9
+ import { XFStreamVoiceManager as M } from "./utils/xunFeiVoice/XFStreamVoiceManager.es.js";
10
10
  import { RecorderManager as R } from "./utils/xunFeiVoice/RecorderManager/RecorderManager.es.js";
11
11
  export {
12
12
  t as AiMessageRender,
13
- l as FunASRManager,
13
+ u as FunASRManager,
14
14
  R as RecorderManager,
15
- i as ScrollController,
16
- n as ShadowDom,
17
- f as VoiceAnimation,
18
- M as XunFeiStreamVoiceManager,
15
+ p as ScrollController,
16
+ a as ShadowDom,
17
+ i as VoiceAnimation,
18
+ M as XFStreamVoiceManager,
19
19
  A as getWebSocketConnectForFunASR,
20
20
  c as useDivAutoScroll,
21
- u as useXunFeiSteamVoiceManager
21
+ g as useXunFeiSteamVoiceManager
22
22
  };
23
23
  //# sourceMappingURL=index.es.js.map
@@ -7,8 +7,20 @@ export class RecorderManager {
7
7
  /** @type {(params: RecordFrameInfo) => void} */
8
8
  onFrameRecorded: (params: RecordFrameInfo) => void;
9
9
  onStart: () => void;
10
+ /** @type {(currStatus: 'stop' | 'closed' | 'initialized' | 'initializing' | 'default') => void} */
11
+ onStop: (currStatus: "stop" | "closed" | "initialized" | "initializing" | "default") => void;
10
12
  /** @type {(data: Array<ArrayBuffer>) => void} */
11
- onStop: (data: Array<ArrayBuffer>) => void;
13
+ onLastFrame: (data: Array<ArrayBuffer>) => void;
14
+ /** @type {'stop' | 'closed' | 'initialized' | 'initializing' | 'default'} */
15
+ status: "stop" | "closed" | "initialized" | "initializing" | "default";
16
+ /** @type {AudioContext} */
17
+ audioContext: AudioContext;
18
+ /** @type {AudioWorkletNode} */
19
+ audioWorkletNode: AudioWorkletNode;
20
+ /** @type {MediaStreamAudioSourceNode} */
21
+ mediaSource: MediaStreamAudioSourceNode;
22
+ /** @type {MediaStream} */
23
+ mediaStream: MediaStream;
12
24
  /**
13
25
  * 开始录音
14
26
  * @param {{
@@ -22,9 +34,6 @@ export class RecorderManager {
22
34
  frameSize?: number;
23
35
  arrayBufferType?: string;
24
36
  }): Promise<void>;
25
- audioWorkletNode: AudioWorkletNode | {
26
- port: Worker;
27
- } | undefined;
28
37
  /**
29
38
  * 创建音频上下文
30
39
  * @private
@@ -35,8 +44,8 @@ export class RecorderManager {
35
44
  * @private
36
45
  */
37
46
  private setupAudioProcessing;
38
- mediaSource: any;
39
47
  /** 停止录音 */
40
48
  stop(): void;
41
- close(): any;
49
+ close(): Promise<void> | undefined;
50
+ destroy(): void;
42
51
  }
@@ -1,38 +1,58 @@
1
- var S = Object.defineProperty;
2
- var g = (o, e, t) => e in o ? S(o, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : o[e] = t;
3
- var c = (o, e, t) => g(o, typeof e != "symbol" ? e + "" : e, t);
4
- import k from "../../../node_modules/.pnpm/co-web-worker@1.0.1/node_modules/co-web-worker/index.es.js";
5
- import y from "./processorSource/processor.worker.es.js";
1
+ var g = Object.defineProperty;
2
+ var k = (s, e, t) => e in s ? g(s, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : s[e] = t;
3
+ var i = (s, e, t) => k(s, typeof e != "symbol" ? e + "" : e, t);
4
+ import y from "../../../node_modules/.pnpm/co-web-worker@1.0.1/node_modules/co-web-worker/index.es.js";
5
+ import M from "./processorSource/processor.worker.es.js";
6
6
  import R from "./processorSource/processor.worklet.es.js";
7
- const { AudioWorkletNode: h } = window, u = !!h;
8
- async function M() {
9
- const o = navigator.mediaDevices ?? {};
10
- if (typeof o.getUserMedia == "function")
11
- return await o.getUserMedia({ audio: !0, video: !1 });
7
+ const { AudioWorkletNode: S } = window, u = !!S;
8
+ async function x() {
9
+ const s = navigator.mediaDevices ?? {};
10
+ if (typeof s.getUserMedia == "function") {
11
+ console.time("mediaDevices.getUserMedia:");
12
+ const e = await s.getUserMedia({ audio: !0, video: !1 });
13
+ return console.timeEnd("mediaDevices.getUserMedia:"), e;
14
+ }
12
15
  if (typeof navigator.getUserMedia == "function")
13
16
  return new Promise((e, t) => {
14
17
  navigator.getUserMedia({ audio: !0, video: !1 }, e, t);
15
18
  });
16
19
  throw new Error("浏览器不支持录音!");
17
20
  }
18
- const w = (o) => {
19
- const e = o === "worker" ? y : R, t = new Blob([e], { type: "application/javascript" });
21
+ const w = (s) => {
22
+ const e = s === "worker" ? M : R, t = new Blob([e], { type: "application/javascript" });
20
23
  return URL.createObjectURL(t);
21
24
  };
22
- async function A(o) {
23
- return u ? (await o.audioWorklet.addModule(w("worklet")), new h(o, "processor-worklet")) : { port: new k(w("worker")) };
25
+ async function C(s) {
26
+ if (u) {
27
+ if (s.state !== "closed")
28
+ return await s.audioWorklet.addModule(w("worklet")), new S(s, "processor-worklet");
29
+ throw new Error("AudioWorkletNode can not be created, when audioContext is closed.");
30
+ }
31
+ return { port: new y(w("worker")) };
24
32
  }
25
- const d = () => {
33
+ const c = () => {
26
34
  };
27
- class v {
35
+ class b {
28
36
  constructor() {
29
37
  /** @type {Array<ArrayBuffer>} */
30
- c(this, "audioBuffers", []);
38
+ i(this, "audioBuffers", []);
31
39
  /** @type {(params: RecordFrameInfo) => void} */
32
- c(this, "onFrameRecorded", d);
33
- c(this, "onStart", d);
40
+ i(this, "onFrameRecorded", c);
41
+ i(this, "onStart", c);
42
+ /** @type {(currStatus: 'stop' | 'closed' | 'initialized' | 'initializing' | 'default') => void} */
43
+ i(this, "onStop", c);
34
44
  /** @type {(data: Array<ArrayBuffer>) => void} */
35
- c(this, "onStop", d);
45
+ i(this, "onLastFrame", c);
46
+ /** @type {'stop' | 'closed' | 'initialized' | 'initializing' | 'default'} */
47
+ i(this, "status", "default");
48
+ /** @type {AudioContext} */
49
+ i(this, "audioContext", null);
50
+ /** @type {AudioWorkletNode} */
51
+ i(this, "audioWorkletNode", null);
52
+ /** @type {MediaStreamAudioSourceNode} */
53
+ i(this, "mediaSource", null);
54
+ /** @type {MediaStream} */
55
+ i(this, "mediaStream", null);
36
56
  }
37
57
  /**
38
58
  * 开始录音
@@ -43,13 +63,20 @@ class v {
43
63
  * }} config 录音配置
44
64
  */
45
65
  async start(e) {
46
- var t, r;
66
+ var t;
67
+ this.status = "initializing";
47
68
  try {
48
69
  this.audioBuffers = [];
49
- const s = await M(), i = this.createAudioContext(s, e.sampleRate);
50
- this.audioWorkletNode = await A(i), this.setupAudioProcessing(i, s, this.audioWorkletNode, e), (t = this.audioContext) == null || t.resume(), (r = this.onStart) == null || r.call(this);
51
- } catch (s) {
52
- throw console.error("Failed to start recording:", s), s;
70
+ const o = await x();
71
+ this.mediaStream = o;
72
+ const r = this.createAudioContext(o, e.sampleRate);
73
+ if (this.audioContext = r, this.audioWorkletNode = await C(r), this.status === "stop" || this.status === "closed") {
74
+ this.close();
75
+ return;
76
+ }
77
+ this.setupAudioProcessing(r, o, this.audioWorkletNode, e), this.status = "initialized", (t = this.onStart) == null || t.call(this);
78
+ } catch (o) {
79
+ throw console.error("Failed to start recording:", o), o;
53
80
  }
54
81
  }
55
82
  /**
@@ -58,61 +85,61 @@ class v {
58
85
  */
59
86
  createAudioContext(e, t) {
60
87
  try {
61
- const r = new (window.AudioContext || window.webkitAudioContext)({
88
+ const o = new (window.AudioContext || window.webkitAudioContext)({
62
89
  sampleRate: t
63
90
  });
64
- return r.createMediaStreamSource(e), r;
91
+ return o.createMediaStreamSource(e), o;
65
92
  } catch {
66
- const s = new (window.AudioContext || window.webkitAudioContext)();
67
- return console.warn(`Using default sample rate: ${s.sampleRate}`), s.createMediaStreamSource(e), s;
93
+ const r = new (window.AudioContext || window.webkitAudioContext)();
94
+ return console.warn(`Using default sample rate: ${r.sampleRate}`), r.createMediaStreamSource(e), r;
68
95
  }
69
96
  }
70
97
  /**
71
98
  * 配置音频处理管线
72
99
  * @private
73
100
  */
74
- setupAudioProcessing(e, t, r, s) {
75
- const i = e.createMediaStreamSource(t);
76
- if (this.mediaSource = i, r.port.postMessage({
101
+ setupAudioProcessing(e, t, o, r) {
102
+ const d = e.createMediaStreamSource(t);
103
+ if (this.mediaSource = d, o.port.postMessage({
77
104
  type: "init",
78
105
  data: {
79
- frameSize: s.frameSize,
80
- toSampleRate: s.sampleRate || e.sampleRate,
106
+ frameSize: r.frameSize,
107
+ toSampleRate: r.sampleRate || e.sampleRate,
81
108
  fromSampleRate: e.sampleRate,
82
- arrayBufferType: s.arrayBufferType || "short16"
109
+ arrayBufferType: r.arrayBufferType || "short16"
83
110
  }
84
- }), r.port.onmessage = (n) => {
85
- var p, f, m, l;
111
+ }), o.port.onmessage = (n) => {
112
+ var l, m, p, f, h;
86
113
  const { data: a } = n;
87
- s.frameSize && this.onFrameRecorded && this.onFrameRecorded(a), a.frameBuffer && this.audioBuffers.push(a.frameBuffer), a.isLastFrame && (u || (p = r.port) == null || p.terminate(), (f = this.mediaSource) == null || f.disconnect(), (m = this.audioContext) == null || m.close(), (l = this.onStop) == null || l.call(this, this.audioBuffers));
114
+ r.frameSize && this.onFrameRecorded && this.onFrameRecorded(a), a.frameBuffer && this.audioBuffers.push(a.frameBuffer), a.isLastFrame && (u || (l = o.port) == null || l.terminate(), (m = this.mediaSource) == null || m.disconnect(), ((p = this.audioContext) == null ? void 0 : p.state) !== "closed" && ((f = this.audioContext) == null || f.close()), (h = this.onLastFrame) == null || h.call(this, this.audioBuffers));
88
115
  }, u)
89
- i.connect(r);
116
+ d.connect(o);
90
117
  else {
91
118
  const n = e.createScriptProcessor(0, 1, 1);
92
119
  n.onaudioprocess = ({ inputBuffer: a }) => {
93
- r.port.postMessage({
120
+ o.port.postMessage({
94
121
  type: "message",
95
122
  data: a.getChannelData(0)
96
123
  });
97
- }, i.connect(n), n.connect(e.destination);
124
+ }, d.connect(n), n.connect(e.destination);
98
125
  }
99
126
  }
100
127
  /** 停止录音 */
101
128
  stop() {
102
- var e;
103
- if ((e = this.audioContext) == null || e.suspend(), !this.audioWorkletNode) {
104
- console.error("Recorder is not started.");
105
- return;
106
- }
107
- this.audioWorkletNode && this.audioWorkletNode.port.postMessage({ type: "stop" });
129
+ var e, t;
130
+ (e = this.onStop) == null || e.call(this, this.status), this.status = "stop", ((t = this.audioContext) == null ? void 0 : t.state) === "running" && this.audioContext.suspend(), this.audioWorkletNode && this.audioWorkletNode.port.postMessage({ type: "stop" });
108
131
  }
109
132
  close() {
110
- var e, t, r;
111
- if (this.audioBuffers = [], (e = this.mediaSource) == null || e.disconnect(), ((t = this.audioContext) == null ? void 0 : t.state) !== "closed")
112
- return (r = this.audioContext) == null ? void 0 : r.close();
133
+ var e, t, o;
134
+ if (this.audioBuffers = [], (e = this.mediaSource) == null || e.disconnect(), this.status = "closed", ((t = this.audioContext) == null ? void 0 : t.state) !== "closed")
135
+ return (o = this.audioContext) == null ? void 0 : o.close();
136
+ }
137
+ destroy() {
138
+ var e, t;
139
+ this.close(), (t = (e = this.mediaStream) == null ? void 0 : e.getTracks()) == null || t.forEach((o) => o.stop());
113
140
  }
114
141
  }
115
142
  export {
116
- v as RecorderManager
143
+ b as RecorderManager
117
144
  };
118
145
  //# sourceMappingURL=RecorderManager.es.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"RecorderManager.es.js","sources":["../../../../src/utils/xunFeiVoice/RecorderManager/RecorderManager.js"],"sourcesContent":["/**\n * 录音管理器 - 用于处理音频录制及处理\n */\nimport CrossOriginWorker from 'co-web-worker';\nimport workerJsString from './processorSource/processor.worker.js?raw';\nimport workletJsString from './processorSource/processor.worklet.js?raw';\n\n//------------------------------ 音频处理核心类 ------------------------------\nconst { AudioWorkletNode } = window;\nconst isAudioWorkletSupported = !!AudioWorkletNode;\n\n/**\n * 获取用户媒体设备权限\n * @returns {Promise<MediaStream>}\n */\nasync function requestMicrophonePermission() {\n const mediaDevices = navigator.mediaDevices ?? {};\n\n if (typeof mediaDevices.getUserMedia === 'function') {\n const stream = await mediaDevices.getUserMedia({ audio: true, video: false });\n return stream;\n }\n\n if (typeof navigator.getUserMedia === 'function') {\n return new Promise((resolve, reject) => {\n navigator.getUserMedia({ audio: true, video: false }, resolve, reject);\n });\n }\n\n throw new Error('浏览器不支持录音!');\n}\n\n/** @type {(type: 'worker' | 'worklet') => string} */\nconst generateWorkletUrl = (type) => {\n const jsStr = type === 'worker' ? workerJsString : workletJsString;\n const blob = new Blob([jsStr], { type: 'application/javascript' });\n const scriptUrl = URL.createObjectURL(blob);\n return scriptUrl;\n};\n\n/**\n * 初始化音频处理器\n * @param {AudioContext} audioContext\n * @returns {Promise<AudioWorkletNode|{port: Worker}>}\n */\nasync function initializeAudioProcessor(audioContext) {\n if (isAudioWorkletSupported) {\n await audioContext.audioWorklet.addModule(generateWorkletUrl('worklet'));\n return new AudioWorkletNode(audioContext, 'processor-worklet');\n }\n\n const worker = new CrossOriginWorker(generateWorkletUrl('worker'));\n return { port: worker };\n}\n\nconst NOOP = () => {};\n/**\n * 录音管理器\n */\n//------------------------------ 录音管理器类 ------------------------------\nexport class RecorderManager {\n /** @type {Array<ArrayBuffer>} */\n audioBuffers = [];\n /** @type {(params: RecordFrameInfo) => void} */\n onFrameRecorded = NOOP;\n onStart = NOOP;\n /** @type {(data: Array<ArrayBuffer>) => void} */\n onStop = NOOP;\n\n constructor() {}\n\n /**\n * 开始录音\n * @param {{\n * sampleRate?: number,\n * frameSize?: number,\n * arrayBufferType?: string\n * }} config 录音配置\n */\n async start(config) {\n try {\n this.audioBuffers = [];\n // 获取音频流并创建处理节点\n const mediaStream = await requestMicrophonePermission();\n const audioContext = this.createAudioContext(mediaStream, config.sampleRate);\n this.audioWorkletNode = await initializeAudioProcessor(audioContext);\n\n // 初始化音频处理\n this.setupAudioProcessing(audioContext, mediaStream, this.audioWorkletNode, config);\n /** 恢复之前暂停播放的音频。 */\n this.audioContext?.resume();\n this.onStart?.();\n } catch (error) {\n console.error('Failed to start recording:', error);\n throw error;\n }\n }\n\n /**\n * 创建音频上下文\n * @private\n */\n createAudioContext(mediaStream, targetSampleRate) {\n try {\n const context = new (window.AudioContext || window.webkitAudioContext)({\n sampleRate: targetSampleRate,\n });\n context.createMediaStreamSource(mediaStream);\n return context;\n } catch (error) {\n // 回退处理:当目标采样率不支持时使用默认采样率\n const fallbackContext = new (window.AudioContext || window.webkitAudioContext)();\n console.warn(`Using default sample rate: ${fallbackContext.sampleRate}`);\n fallbackContext.createMediaStreamSource(mediaStream);\n return fallbackContext;\n }\n }\n\n /**\n * 配置音频处理管线\n * @private\n */\n setupAudioProcessing(audioContext, mediaStream, audioWorkletNode, config) {\n // 创建媒体流源节点\n const mediaSource = audioContext.createMediaStreamSource(mediaStream);\n this.mediaSource = mediaSource;\n\n // 配置处理器通信\n audioWorkletNode.port.postMessage({\n type: 'init',\n data: {\n frameSize: config.frameSize,\n toSampleRate: config.sampleRate || audioContext.sampleRate,\n fromSampleRate: audioContext.sampleRate,\n arrayBufferType: config.arrayBufferType || 'short16',\n },\n });\n\n // 处理音频数据回调\n audioWorkletNode.port.onmessage = (e) => {\n const { data } = e;\n if (config.frameSize && this.onFrameRecorded) {\n this.onFrameRecorded(data);\n }\n\n if (data.frameBuffer) {\n this.audioBuffers.push(data.frameBuffer);\n }\n\n if (data.isLastFrame) {\n if (!isAudioWorkletSupported) {\n audioWorkletNode.port?.terminate();\n }\n this.mediaSource?.disconnect();\n this.audioContext?.close();\n this.onStop?.(this.audioBuffers);\n }\n };\n\n // 连接音频节点\n if (isAudioWorkletSupported) {\n mediaSource.connect(audioWorkletNode);\n } else {\n const scriptProcessor = audioContext.createScriptProcessor(0, 1, 1);\n scriptProcessor.onaudioprocess = ({ inputBuffer }) => {\n audioWorkletNode.port.postMessage({\n type: 'message',\n data: inputBuffer.getChannelData(0),\n });\n };\n mediaSource.connect(scriptProcessor);\n scriptProcessor.connect(audioContext.destination);\n }\n }\n\n /** 停止录音 */\n stop() {\n /** 暂停音频上下文对象中的进度,并暂时剥离进程对音频设备硬件的访问权限,减少 CPU 和电池的使用 */\n this.audioContext?.suspend();\n if (!this.audioWorkletNode) {\n console.error('Recorder is not started.');\n return;\n }\n if (this.audioWorkletNode) {\n this.audioWorkletNode.port.postMessage({ type: 'stop' });\n }\n }\n\n close() {\n this.audioBuffers = [];\n this.mediaSource?.disconnect();\n if (this.audioContext?.state !== 'closed') {\n return this.audioContext?.close();\n }\n }\n}\n"],"names":["AudioWorkletNode","isAudioWorkletSupported","requestMicrophonePermission","mediaDevices","resolve","reject","generateWorkletUrl","type","jsStr","workerJsString","workletJsString","blob","initializeAudioProcessor","audioContext","CrossOriginWorker","NOOP","RecorderManager","__publicField","config","mediaStream","_a","_b","error","targetSampleRate","context","fallbackContext","audioWorkletNode","mediaSource","e","data","_c","_d","scriptProcessor","inputBuffer"],"mappings":"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"}
1
+ {"version":3,"file":"RecorderManager.es.js","sources":["../../../../src/utils/xunFeiVoice/RecorderManager/RecorderManager.js"],"sourcesContent":["/**\n * 录音管理器 - 用于处理音频录制及处理\n */\nimport CrossOriginWorker from 'co-web-worker';\nimport workerJsString from './processorSource/processor.worker.js?raw';\nimport workletJsString from './processorSource/processor.worklet.js?raw';\n\nconst { AudioWorkletNode } = window;\nconst isAudioWorkletSupported = !!AudioWorkletNode;\n\n/**\n * 获取用户媒体设备权限\n * @returns {Promise<MediaStream>}\n */\nasync function requestMicrophonePermission() {\n const mediaDevices = navigator.mediaDevices ?? {};\n\n if (typeof mediaDevices.getUserMedia === 'function') {\n console.time('mediaDevices.getUserMedia:');\n const stream = await mediaDevices.getUserMedia({ audio: true, video: false });\n console.timeEnd('mediaDevices.getUserMedia:');\n return stream;\n }\n\n if (typeof navigator.getUserMedia === 'function') {\n return new Promise((resolve, reject) => {\n navigator.getUserMedia({ audio: true, video: false }, resolve, reject);\n });\n }\n\n throw new Error('浏览器不支持录音!');\n}\n\n/** @type {(type: 'worker' | 'worklet') => string} */\nconst generateWorkletUrl = (type) => {\n const jsStr = type === 'worker' ? workerJsString : workletJsString;\n const blob = new Blob([jsStr], { type: 'application/javascript' });\n const scriptUrl = URL.createObjectURL(blob);\n return scriptUrl;\n};\n\n/**\n * 初始化音频处理器\n * @param {AudioContext} audioContext\n * @returns {Promise<AudioWorkletNode|{port: Worker}>}\n */\nasync function initializeAudioProcessor(audioContext) {\n if (isAudioWorkletSupported) {\n if (audioContext.state !== 'closed') {\n await audioContext.audioWorklet.addModule(generateWorkletUrl('worklet'));\n return new AudioWorkletNode(audioContext, 'processor-worklet');\n }\n throw new Error('AudioWorkletNode can not be created, when audioContext is closed.');\n }\n\n const worker = new CrossOriginWorker(generateWorkletUrl('worker'));\n return { port: worker };\n}\n\nconst NOOP = () => {};\n/**\n * 录音管理器\n */\nexport class RecorderManager {\n /** @type {Array<ArrayBuffer>} */\n audioBuffers = [];\n /** @type {(params: RecordFrameInfo) => void} */\n onFrameRecorded = NOOP;\n onStart = NOOP;\n /** @type {(currStatus: 'stop' | 'closed' | 'initialized' | 'initializing' | 'default') => void} */\n onStop = NOOP;\n /** @type {(data: Array<ArrayBuffer>) => void} */\n onLastFrame = NOOP;\n /** @type {'stop' | 'closed' | 'initialized' | 'initializing' | 'default'} */\n status = 'default';\n /** @type {AudioContext} */\n audioContext = null;\n /** @type {AudioWorkletNode} */\n audioWorkletNode = null;\n /** @type {MediaStreamAudioSourceNode} */\n mediaSource = null;\n /** @type {MediaStream} */\n mediaStream = null;\n\n constructor() {}\n\n /**\n * 开始录音\n * @param {{\n * sampleRate?: number,\n * frameSize?: number,\n * arrayBufferType?: string\n * }} config 录音配置\n */\n async start(config) {\n this.status = 'initializing';\n try {\n this.audioBuffers = [];\n // 获取音频流并创建处理节点\n const mediaStream = await requestMicrophonePermission();\n this.mediaStream = mediaStream;\n const audioContext = this.createAudioContext(mediaStream, config.sampleRate);\n this.audioContext = audioContext;\n this.audioWorkletNode = await initializeAudioProcessor(audioContext);\n // 防止开始后立刻结束,资源没有得到释放。\n if (this.status === 'stop' || this.status === 'closed') {\n this.close();\n return;\n }\n\n // 初始化音频处理\n this.setupAudioProcessing(audioContext, mediaStream, this.audioWorkletNode, config);\n\n this.status = 'initialized';\n this.onStart?.();\n } catch (error) {\n console.error('Failed to start recording:', error);\n throw error;\n }\n }\n\n /**\n * 创建音频上下文\n * @private\n */\n createAudioContext(mediaStream, targetSampleRate) {\n try {\n const context = new (window.AudioContext || window.webkitAudioContext)({\n sampleRate: targetSampleRate,\n });\n context.createMediaStreamSource(mediaStream);\n return context;\n } catch (error) {\n // 回退处理:当目标采样率不支持时使用默认采样率\n const fallbackContext = new (window.AudioContext || window.webkitAudioContext)();\n console.warn(`Using default sample rate: ${fallbackContext.sampleRate}`);\n fallbackContext.createMediaStreamSource(mediaStream);\n return fallbackContext;\n }\n }\n\n /**\n * 配置音频处理管线\n * @private\n */\n setupAudioProcessing(audioContext, mediaStream, audioWorkletNode, config) {\n // 创建媒体流源节点\n const mediaSource = audioContext.createMediaStreamSource(mediaStream);\n this.mediaSource = mediaSource;\n\n // 配置处理器通信\n audioWorkletNode.port.postMessage({\n type: 'init',\n data: {\n frameSize: config.frameSize,\n toSampleRate: config.sampleRate || audioContext.sampleRate,\n fromSampleRate: audioContext.sampleRate,\n arrayBufferType: config.arrayBufferType || 'short16',\n },\n });\n\n // 处理音频数据回调\n audioWorkletNode.port.onmessage = (e) => {\n const { data } = e;\n if (config.frameSize && this.onFrameRecorded) {\n this.onFrameRecorded(data);\n }\n\n if (data.frameBuffer) {\n this.audioBuffers.push(data.frameBuffer);\n }\n\n if (data.isLastFrame) {\n if (!isAudioWorkletSupported) {\n audioWorkletNode.port?.terminate();\n }\n this.mediaSource?.disconnect();\n if (this.audioContext?.state !== 'closed') {\n this.audioContext?.close();\n }\n this.onLastFrame?.(this.audioBuffers);\n }\n };\n\n // 连接音频节点\n if (isAudioWorkletSupported) {\n mediaSource.connect(audioWorkletNode);\n } else {\n const scriptProcessor = audioContext.createScriptProcessor(0, 1, 1);\n scriptProcessor.onaudioprocess = ({ inputBuffer }) => {\n audioWorkletNode.port.postMessage({\n type: 'message',\n data: inputBuffer.getChannelData(0),\n });\n };\n mediaSource.connect(scriptProcessor);\n scriptProcessor.connect(audioContext.destination);\n }\n }\n\n /** 停止录音 */\n stop() {\n this.onStop?.(this.status);\n this.status = 'stop';\n /** 暂停音频上下文对象中的进度,并暂时剥离进程对音频设备硬件的访问权限,减少 CPU 和电池的使用 */\n if (this.audioContext?.state === 'running') {\n this.audioContext.suspend();\n }\n if (this.audioWorkletNode) {\n this.audioWorkletNode.port.postMessage({ type: 'stop' });\n }\n }\n\n close() {\n this.audioBuffers = [];\n this.mediaSource?.disconnect();\n this.status = 'closed';\n if (this.audioContext?.state !== 'closed') {\n return this.audioContext?.close();\n }\n }\n\n destroy() {\n this.close();\n this.mediaStream?.getTracks()?.forEach((track) => track.stop());\n }\n}\n"],"names":["AudioWorkletNode","isAudioWorkletSupported","requestMicrophonePermission","mediaDevices","stream","resolve","reject","generateWorkletUrl","type","jsStr","workerJsString","workletJsString","blob","initializeAudioProcessor","audioContext","CrossOriginWorker","NOOP","RecorderManager","__publicField","config","mediaStream","_a","error","targetSampleRate","context","fallbackContext","audioWorkletNode","mediaSource","e","data","_b","_c","_d","_e","scriptProcessor","inputBuffer","track"],"mappings":"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"}
@@ -2,7 +2,7 @@ import { RecorderManager } from './RecorderManager';
2
2
  import { OnMessageInfo, RecordFrameInfo } from './types';
3
3
 
4
4
  type errorType = 'socketConnect' | 'message' | 'record';
5
- export interface XunFeiStreamVoiceManagerConfig {
5
+ export interface XFStreamVoiceManagerConfig {
6
6
  onError: (errInfo: {
7
7
  type: errorType;
8
8
  message: string;
@@ -21,23 +21,33 @@ export interface XunFeiStreamVoiceManagerConfig {
21
21
  */
22
22
  onRecordStart?: () => void;
23
23
  /**
24
- * 录音结束的回调
24
+ * 录音开始的回调
25
+ */
26
+ onRecordStop?: () => void;
27
+ /**
28
+ * 录音最后一帧的回调。
29
+ * @param totalData 所有的录音数据, ArrayBuffer[]。
25
30
  */
26
- onRecordStop?: (data: ArrayBuffer[]) => void;
31
+ onLastFrame?: (totalData: ArrayBuffer[]) => void;
27
32
  /**
28
33
  * 获取链接讯飞语音识别地址的 url
29
34
  * @returns websocket url。详情查看讯飞文档里的鉴权方法:https://www.xfyun.cn/doc/asr/voicedictation/API.html#%E6%8E%A5%E5%8F%A3%E8%B0%83%E7%94%A8%E6%B5%81%E7%A8%8B
30
35
  */
31
36
  getWebSocketUrl?: () => string;
32
37
  }
33
- type SocketStatus = 'UNDEFINED' | 'CONNECTING' | 'OPEN' | 'CLOSING' | 'CLOSED';
34
38
  /**
35
39
  * 讯飞流式语音识别
40
+ * 大致流程:
41
+ * 1. recorder start 后开始接受录音,通过 recorder.onFrameRecorded 把数据透出给上层。数据中 isLastFrame 表示是否最后一次数据, frameBuffer 表示真实数据。
42
+ * 2. 上层拿到录音数据后,通过 ws 发送给讯飞。如果 isLastFrame 为 true 时,给讯飞的数据中 status 为 2。如果 isLastFrame 为 false 时,给讯飞的数据中 status 为 1。
43
+ * 3. ws 返回的数据,通过 renderResult 处理。数据中 status 为 2 表示本次录音数据识别结束了,这时候需要关闭 ws,避免消耗资源。且这两个情况服务端会主动断开连接(整个会话时长最多持续60s,或者超过10s未发送数据)。
44
+ * 讯飞流式语音文档:https://www.xfyun.cn/doc/asr/voicedictation/API.html#%E6%8E%A5%E5%8F%A3%E8%B0%83%E7%94%A8%E6%B5%81%E7%A8%8B
45
+ * 4. 断开链接后会关闭 recorder ,重置录音等数据,方便下次使用。
46
+ * 备注:使用时,建议在收到 onWsClose 事件后,才让前端恢复到可以再次输入录音的状态
36
47
  */
37
- export declare class XunFeiStreamVoiceManager {
38
- config: XunFeiStreamVoiceManagerConfig;
48
+ export declare class XFStreamVoiceManager {
49
+ config: XFStreamVoiceManagerConfig;
39
50
  wsInstance: WebSocket | null;
40
- status: SocketStatus;
41
51
  recorder: RecorderManager | null;
42
52
  isDestroy: boolean;
43
53
  resultText: string;
@@ -46,17 +56,17 @@ export declare class XunFeiStreamVoiceManager {
46
56
  startTimeStamp: number;
47
57
  recordStatus: 'start' | 'stop';
48
58
  readyToSend: boolean;
49
- constructor(config: XunFeiStreamVoiceManagerConfig);
59
+ constructor(config: XFStreamVoiceManagerConfig);
50
60
  renderResult(resultData: string): void;
51
- setConfig(newConfig: XunFeiStreamVoiceManagerConfig): void;
61
+ setConfig(newConfig: XFStreamVoiceManagerConfig): void;
52
62
  sendMessage(message: string): void;
53
63
  recordConfigSend(): Promise<void>;
54
64
  startNewSocket(): void;
55
- recordProcess({ isLastFrame, frameBuffer }: RecordFrameInfo): void;
65
+ onFrameRecorded({ isLastFrame, frameBuffer }: RecordFrameInfo): void;
56
66
  initializeRecorder(): void;
57
- changeBtnStatus(status: SocketStatus): void;
58
67
  startRecord(): void;
59
68
  stopRecord(): Promise<void>;
69
+ closeWs(): void;
60
70
  /** 开启持续监听 */
61
71
  startContinueRecord(): void;
62
72
  stopContinueRecord(): void;
@@ -0,0 +1,184 @@
1
+ var m = Object.defineProperty;
2
+ var p = (i, e, t) => e in i ? m(i, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : i[e] = t;
3
+ var o = (i, e, t) => p(i, typeof e != "symbol" ? e + "" : e, t);
4
+ import c from "../../node_modules/.pnpm/crypto-js@4.2.0/node_modules/crypto-js/index.es.js";
5
+ import { toBase64 as S } from "./utils.es.js";
6
+ import { RecorderManager as w } from "./RecorderManager/RecorderManager.es.js";
7
+ let d = 0;
8
+ const T = "93b73e33", y = "ZGJhMzQ5ZTJlMDgyYmE1ZTFlZDlmYjg0", R = "fe43de7a071e3a32ec03c6f09fe7bedf";
9
+ function x() {
10
+ const i = "wss://iat-api.xfyun.cn/v2/iat", e = "iat-api.xfyun.cn", t = R, r = y, s = (/* @__PURE__ */ new Date()).toUTCString(), n = "hmac-sha256", a = "host date request-line", h = `host: ${e}
11
+ date: ${s}
12
+ GET /v2/iat HTTP/1.1`, l = c.HmacSHA256(h, r), u = c.enc.Base64.stringify(l), g = `api_key="${t}", algorithm="${n}", headers="${a}", signature="${u}"`, f = window.btoa(g);
13
+ return `${i}?authorization=${f}&date=${s}&host=${e}`;
14
+ }
15
+ class D {
16
+ constructor(e) {
17
+ o(this, "wsInstance", null);
18
+ o(this, "recorder", null);
19
+ o(this, "isDestroy", !1);
20
+ o(this, "resultText", "");
21
+ o(this, "resultTextTemp", "");
22
+ o(this, "continueRecord", !1);
23
+ o(this, "startTimeStamp", 0);
24
+ o(this, "recordStatus", "stop");
25
+ o(this, "readyToSend", !1);
26
+ this.config = e, this.initializeRecorder();
27
+ }
28
+ renderResult(e) {
29
+ const t = JSON.parse(e);
30
+ if (t.data && t.data.result) {
31
+ const r = t.data.result;
32
+ let s = "";
33
+ const n = r.ws;
34
+ for (let a = 0; a < n.length; a++)
35
+ s = s + n[a].cw[0].w;
36
+ r.pgs ? (r.pgs === "apd" && (this.resultText = this.resultTextTemp), this.resultTextTemp = this.resultText + s) : this.resultText = this.resultText + s;
37
+ }
38
+ if (t.code === 0 && t.data.status === 2) {
39
+ this.config.onMessage({
40
+ message: this.resultText,
41
+ isLatest: !0,
42
+ tempMessage: this.resultTextTemp
43
+ }), this.closeWs();
44
+ return;
45
+ }
46
+ t.code === 0 && t.data.status === 1 && this.config.onMessage({
47
+ message: this.resultText,
48
+ isLatest: !1,
49
+ tempMessage: this.resultTextTemp
50
+ }), t.code !== 0 && (this.closeWs(), this.config.onError({ type: "message", message: e }));
51
+ }
52
+ setConfig(e) {
53
+ var t;
54
+ this.config = {
55
+ ...this.config,
56
+ ...e
57
+ }, (t = this.recorder) == null || t.close(), this.initializeRecorder();
58
+ }
59
+ sendMessage(e) {
60
+ var t;
61
+ ((t = this.wsInstance) == null ? void 0 : t.readyState) === WebSocket.OPEN && this.readyToSend && this.wsInstance.send(e);
62
+ }
63
+ async recordConfigSend() {
64
+ var e;
65
+ if (!this.isDestroy) {
66
+ if (this.recordStatus === "stop") {
67
+ this.closeWs();
68
+ return;
69
+ }
70
+ try {
71
+ await ((e = this.recorder) == null ? void 0 : e.start({
72
+ sampleRate: 16e3,
73
+ frameSize: 1280
74
+ }));
75
+ const t = {
76
+ common: {
77
+ app_id: T
78
+ },
79
+ business: {
80
+ language: "zh_cn",
81
+ domain: "iat",
82
+ accent: "mandarin",
83
+ vad_eos: 5e3,
84
+ dwa: "wpgs"
85
+ },
86
+ data: {
87
+ status: 0,
88
+ format: "audio/L16;rate=16000",
89
+ encoding: "raw"
90
+ }
91
+ };
92
+ this.readyToSend = !0, this.sendMessage(JSON.stringify(t));
93
+ } catch (t) {
94
+ console.error("record error:", t), this.config.onError({ type: "record", message: "录音失败, 请检查权限!" });
95
+ }
96
+ }
97
+ }
98
+ startNewSocket() {
99
+ var t, r;
100
+ console.log(
101
+ "%c ❤️ love ==== start new socket:",
102
+ "color: red; font-size: 16px;",
103
+ d,
104
+ (/* @__PURE__ */ new Date()).toUTCString()
105
+ ), d++;
106
+ const e = ((r = (t = this.config).getWebSocketUrl) == null ? void 0 : r.call(t)) || x();
107
+ if ("WebSocket" in window)
108
+ this.wsInstance = new WebSocket(e);
109
+ else {
110
+ this.config.onError({ type: "socketConnect", message: "浏览器不支持 WebSocket" });
111
+ return;
112
+ }
113
+ this.wsInstance.onmessage = (s) => {
114
+ this.renderResult(s.data);
115
+ }, this.wsInstance.onerror = (s) => {
116
+ console.error("socket error:", s);
117
+ }, this.wsInstance.onclose = async () => {
118
+ var s;
119
+ this.config.onWsClose(), this.continueRecord && (this.readyToSend = !1, await ((s = this.recorder) == null ? void 0 : s.close()), this.startRecord());
120
+ }, this.wsInstance.onopen = () => {
121
+ this.recordConfigSend();
122
+ };
123
+ }
124
+ onFrameRecorded({ isLastFrame: e, frameBuffer: t }) {
125
+ this.isDestroy || this.wsInstance && (this.wsInstance.readyState === this.wsInstance.OPEN && this.sendMessage(
126
+ JSON.stringify({
127
+ data: {
128
+ status: e ? 2 : 1,
129
+ format: "audio/L16;rate=16000",
130
+ encoding: "raw",
131
+ audio: S(t)
132
+ }
133
+ })
134
+ ), e && (this.readyToSend = !1));
135
+ }
136
+ initializeRecorder() {
137
+ this.recorder = new w(), this.recorder.onFrameRecorded = (e) => {
138
+ this.onFrameRecorded(e);
139
+ }, this.recorder.onLastFrame = (e) => {
140
+ var t, r;
141
+ (r = (t = this.config).onLastFrame) == null || r.call(t, e);
142
+ }, this.recorder.onStop = () => {
143
+ var e, t;
144
+ (t = (e = this.config).onRecordStop) == null || t.call(e);
145
+ }, this.recorder.onStart = () => {
146
+ var e, t;
147
+ (t = (e = this.config).onRecordStart) == null || t.call(e);
148
+ };
149
+ }
150
+ startRecord() {
151
+ this.isDestroy || (this.recordStatus = "start", this.startTimeStamp = Date.now(), this.resultText = "", this.resultTextTemp = "", this.startNewSocket());
152
+ }
153
+ async stopRecord() {
154
+ var e, t;
155
+ this.recordStatus = "stop", console.log("stop record ====", Date.now() - this.startTimeStamp, (e = this.recorder) == null ? void 0 : e.status), (t = this.recorder) == null || t.stop(), setTimeout(() => {
156
+ this.closeWs();
157
+ }, 3e3);
158
+ }
159
+ closeWs() {
160
+ var e, t;
161
+ if (((e = this.wsInstance) == null ? void 0 : e.readyState) !== WebSocket.CLOSED)
162
+ try {
163
+ (t = this.wsInstance) == null || t.close();
164
+ } catch {
165
+ }
166
+ }
167
+ /** 开启持续监听 */
168
+ startContinueRecord() {
169
+ this.continueRecord = !0, this.startRecord();
170
+ }
171
+ stopContinueRecord() {
172
+ this.continueRecord = !1, this.stopRecord();
173
+ }
174
+ destroy() {
175
+ this.isDestroy = !0, setTimeout(() => {
176
+ var e, t;
177
+ this.closeWs(), (e = this.recorder) == null || e.stop(), (t = this.recorder) == null || t.destroy();
178
+ }, 100);
179
+ }
180
+ }
181
+ export {
182
+ D as XFStreamVoiceManager
183
+ };
184
+ //# sourceMappingURL=XFStreamVoiceManager.es.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"XFStreamVoiceManager.es.js","sources":["../../../src/utils/xunFeiVoice/XFStreamVoiceManager.ts"],"sourcesContent":["import CryptoJS from 'crypto-js';\nimport { RecorderManager } from './RecorderManager';\nimport type { OnMessageInfo, RecordFrameInfo } from './types';\nimport { toBase64 } from './utils';\n\ntype errorType = 'socketConnect' | 'message' | 'record';\nlet startCount = 0;\n\nexport interface XFStreamVoiceManagerConfig {\n onError: (errInfo: { type: errorType; message: string }) => void;\n /** ws 收到消息的回调\n * @param info.message 消息内容\n * @param info.isLatest 是否是最后一条消息。 isLatest 为 true 时, message 为完整的语音识别结果。\n */\n onMessage: (info: OnMessageInfo) => void;\n /** webSocket 关闭的回调。\n * 每次录音都会新建一个 webSocket 连接。录音结束后关闭 webSocket\n */\n onWsClose: () => void;\n /**\n * 录音开始的回调\n */\n onRecordStart?: () => void;\n /**\n * 录音开始的回调\n */\n onRecordStop?: () => void;\n /**\n * 录音最后一帧的回调。\n * @param totalData 所有的录音数据, ArrayBuffer[]。\n */\n onLastFrame?: (totalData: ArrayBuffer[]) => void;\n /**\n * 获取链接讯飞语音识别地址的 url\n * @returns websocket url。详情查看讯飞文档里的鉴权方法:https://www.xfyun.cn/doc/asr/voicedictation/API.html#%E6%8E%A5%E5%8F%A3%E8%B0%83%E7%94%A8%E6%B5%81%E7%A8%8B\n */\n getWebSocketUrl?: () => string;\n}\n\n/** 现在前端mock,后续移到接口下发 */\nconst APPID = '93b73e33';\nconst API_SECRET = 'ZGJhMzQ5ZTJlMDgyYmE1ZTFlZDlmYjg0';\nconst API_KEY = 'fe43de7a071e3a32ec03c6f09fe7bedf';\n/**\n * 获取websocket url\n * 该接口需要后端提供,这里为了方便前端处理\n */\nfunction getWebSocketUrl() {\n // 请求地址根据语种不同变化\n const url = 'wss://iat-api.xfyun.cn/v2/iat';\n const host = 'iat-api.xfyun.cn';\n const apiKey = API_KEY;\n const apiSecret = API_SECRET;\n const date = new Date().toUTCString();\n const algorithm = 'hmac-sha256';\n const headers = 'host date request-line';\n const signatureOrigin = `host: ${host}\\ndate: ${date}\\nGET /v2/iat HTTP/1.1`;\n const signatureSha = CryptoJS.HmacSHA256(signatureOrigin, apiSecret);\n const signature = CryptoJS.enc.Base64.stringify(signatureSha);\n const authorizationOrigin = `api_key=\"${apiKey}\", algorithm=\"${algorithm}\", headers=\"${headers}\", signature=\"${signature}\"`;\n const authorization = window.btoa(authorizationOrigin);\n return `${url}?authorization=${authorization}&date=${date}&host=${host}`;\n}\n\n/**\n * 讯飞流式语音识别\n * 大致流程:\n * 1. recorder start 后开始接受录音,通过 recorder.onFrameRecorded 把数据透出给上层。数据中 isLastFrame 表示是否最后一次数据, frameBuffer 表示真实数据。\n * 2. 上层拿到录音数据后,通过 ws 发送给讯飞。如果 isLastFrame 为 true 时,给讯飞的数据中 status 为 2。如果 isLastFrame 为 false 时,给讯飞的数据中 status 为 1。\n * 3. ws 返回的数据,通过 renderResult 处理。数据中 status 为 2 表示本次录音数据识别结束了,这时候需要关闭 ws,避免消耗资源。且这两个情况服务端会主动断开连接(整个会话时长最多持续60s,或者超过10s未发送数据)。\n * 讯飞流式语音文档:https://www.xfyun.cn/doc/asr/voicedictation/API.html#%E6%8E%A5%E5%8F%A3%E8%B0%83%E7%94%A8%E6%B5%81%E7%A8%8B\n * 4. 断开链接后会关闭 recorder ,重置录音等数据,方便下次使用。\n * 备注:使用时,建议在收到 onWsClose 事件后,才让前端恢复到可以再次输入录音的状态\n */\nexport class XFStreamVoiceManager {\n wsInstance: WebSocket | null = null;\n recorder: RecorderManager | null = null;\n isDestroy = false;\n resultText = '';\n resultTextTemp = '';\n continueRecord = false;\n startTimeStamp = 0;\n recordStatus: 'start' | 'stop' = 'stop';\n readyToSend = false;\n\n constructor(public config: XFStreamVoiceManagerConfig) {\n this.initializeRecorder();\n }\n renderResult(resultData: string) {\n // 识别结束\n const jsonData = JSON.parse(resultData);\n if (jsonData.data && jsonData.data.result) {\n const data = jsonData.data.result;\n let str = '';\n const ws = data.ws;\n for (let i = 0; i < ws.length; i++) {\n str = str + ws[i].cw[0].w;\n }\n // 开启 wpgs 会有此字段(前提:在控制台开通动态修正功能)\n // 取值为 \"apd\"时表示该片结果是追加到前面的最终结果;取值为\"rpl\" 时表示替换前面的部分结果,替换范围为rg字段\n if (data.pgs) {\n if (data.pgs === 'apd') {\n // 将resultTextTemp同步给resultText\n this.resultText = this.resultTextTemp;\n }\n // 将结果存储在resultTextTemp中\n this.resultTextTemp = this.resultText + str;\n } else {\n this.resultText = this.resultText + str;\n }\n }\n /**\n * 识别结果是否结束标识:\n * 0:识别的第一块结果。\n * 1:识别中间结果。\n * 2:识别最后一块结果\n */\n if (jsonData.code === 0 && jsonData.data.status === 2) {\n this.config.onMessage({\n message: this.resultText,\n isLatest: true,\n tempMessage: this.resultTextTemp,\n });\n this.closeWs();\n return;\n }\n if (jsonData.code === 0 && jsonData.data.status === 1) {\n this.config.onMessage({\n message: this.resultText,\n isLatest: false,\n tempMessage: this.resultTextTemp,\n });\n }\n // code 不为0时表示出错\n if (jsonData.code !== 0) {\n this.closeWs();\n this.config.onError({ type: 'message', message: resultData });\n }\n }\n\n setConfig(newConfig: XFStreamVoiceManagerConfig) {\n this.config = {\n ...this.config,\n ...newConfig,\n };\n this.recorder?.close();\n this.initializeRecorder();\n }\n sendMessage(message: string) {\n if (this.wsInstance?.readyState === WebSocket.OPEN && this.readyToSend) {\n this.wsInstance.send(message);\n }\n }\n\n async recordConfigSend() {\n if (this.isDestroy) {\n return;\n }\n if (this.recordStatus === 'stop') {\n this.closeWs();\n return;\n }\n try {\n // 开始录音\n await this.recorder?.start({\n sampleRate: 16000,\n frameSize: 1280,\n });\n const params = {\n common: {\n app_id: APPID,\n },\n business: {\n language: 'zh_cn',\n domain: 'iat',\n accent: 'mandarin',\n vad_eos: 5000,\n dwa: 'wpgs',\n },\n data: {\n status: 0,\n format: 'audio/L16;rate=16000',\n encoding: 'raw',\n },\n };\n this.readyToSend = true;\n this.sendMessage(JSON.stringify(params));\n } catch (error) {\n console.error('record error:', error);\n this.config.onError({ type: 'record', message: '录音失败, 请检查权限!' });\n }\n }\n\n startNewSocket() {\n console.log(\n '%c ❤️ love ==== start new socket:',\n 'color: red; font-size: 16px;',\n startCount,\n new Date().toUTCString(),\n );\n startCount++;\n const websocketUrl = this.config.getWebSocketUrl?.() || getWebSocketUrl();\n if ('WebSocket' in window) {\n this.wsInstance = new WebSocket(websocketUrl);\n } else {\n this.config.onError({ type: 'socketConnect', message: '浏览器不支持 WebSocket' });\n return;\n }\n this.wsInstance.onmessage = (e) => {\n // 处理返回数据\n this.renderResult(e.data);\n };\n this.wsInstance.onerror = (e) => {\n console.error('socket error:', e);\n };\n this.wsInstance.onclose = async () => {\n this.config.onWsClose();\n if (this.continueRecord) {\n this.readyToSend = false;\n await this.recorder?.close();\n this.startRecord();\n }\n };\n this.wsInstance.onopen = () => {\n this.recordConfigSend();\n };\n }\n\n onFrameRecorded({ isLastFrame, frameBuffer }: RecordFrameInfo) {\n if (this.isDestroy) {\n return;\n }\n if (!this.wsInstance) {\n return;\n }\n if (this.wsInstance.readyState === this.wsInstance.OPEN) {\n this.sendMessage(\n JSON.stringify({\n data: {\n status: isLastFrame ? 2 : 1,\n format: 'audio/L16;rate=16000',\n encoding: 'raw',\n audio: toBase64(frameBuffer),\n },\n }),\n );\n }\n if (isLastFrame) {\n this.readyToSend = false;\n }\n }\n\n initializeRecorder() {\n this.recorder = new RecorderManager();\n this.recorder.onFrameRecorded = (data: RecordFrameInfo) => {\n this.onFrameRecorded(data);\n };\n this.recorder.onLastFrame = (data: ArrayBuffer[]) => {\n this.config.onLastFrame?.(data);\n };\n this.recorder.onStop = () => {\n this.config.onRecordStop?.();\n };\n this.recorder.onStart = () => {\n this.config.onRecordStart?.();\n };\n }\n\n startRecord() {\n if (this.isDestroy) {\n return;\n }\n this.recordStatus = 'start';\n this.startTimeStamp = Date.now();\n this.resultText = '';\n this.resultTextTemp = '';\n this.startNewSocket();\n }\n\n async stopRecord() {\n this.recordStatus = 'stop';\n console.log('stop record ====', Date.now() - this.startTimeStamp, this.recorder?.status);\n this.recorder?.stop();\n // 如果 recorder 还没有初始化完成就关闭了,那么需要手动关闭 ws,否则在收到最后一帧消息时关闭ws\n // if (this.recorder?.status !== 'initialized') {\n // this.closeWs();\n // }\n // 这里为了防止 ws 没有返回结束的数据,导致 ws 一直没关,所以加个定时器,5s 后手动关闭\n setTimeout(() => {\n this.closeWs();\n }, 3000);\n }\n\n closeWs() {\n if (this.wsInstance?.readyState !== WebSocket.CLOSED) {\n try {\n this.wsInstance?.close();\n } catch {\n // 有的浏览器在 ws 为 connecting 的时候 close 会报错,这里忽略掉\n }\n }\n }\n /** 开启持续监听 */\n startContinueRecord() {\n this.continueRecord = true;\n this.startRecord();\n }\n\n stopContinueRecord() {\n this.continueRecord = false;\n this.stopRecord();\n }\n\n destroy() {\n this.isDestroy = true;\n setTimeout(() => {\n this.closeWs();\n this.recorder?.stop();\n this.recorder?.destroy();\n }, 100);\n }\n}\n"],"names":["startCount","APPID","API_SECRET","API_KEY","getWebSocketUrl","url","host","apiKey","apiSecret","date","algorithm","headers","signatureOrigin","signatureSha","CryptoJS","signature","authorizationOrigin","authorization","XFStreamVoiceManager","config","__publicField","resultData","jsonData","data","str","ws","i","newConfig","_a","message","params","error","websocketUrl","_b","e","isLastFrame","frameBuffer","toBase64","RecorderManager"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
- export * from './XunFeiStreamVoiceManager';
1
+ export * from './XFStreamVoiceManager';
2
2
  export * from './RecorderManager';
3
3
  export * from './types';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easyv/biz-components",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
4
4
  "type": "module",
5
5
  "main": "dist/index.es.js",
6
6
  "module": "dist/index.es.js",
@@ -1,176 +0,0 @@
1
- var S = Object.defineProperty;
2
- var m = (i, t, e) => t in i ? S(i, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : i[t] = e;
3
- var o = (i, t, e) => m(i, typeof t != "symbol" ? t + "" : t, e);
4
- import d from "../../node_modules/.pnpm/crypto-js@4.2.0/node_modules/crypto-js/index.es.js";
5
- import { toBase64 as p } from "./utils.es.js";
6
- import { RecorderManager as w } from "./RecorderManager/RecorderManager.es.js";
7
- let u = 0;
8
- const T = "93b73e33", y = "ZGJhMzQ5ZTJlMDgyYmE1ZTFlZDlmYjg0", I = "fe43de7a071e3a32ec03c6f09fe7bedf";
9
- function R() {
10
- const i = "wss://iat-api.xfyun.cn/v2/iat", t = "iat-api.xfyun.cn", e = I, s = y, r = (/* @__PURE__ */ new Date()).toUTCString(), n = "hmac-sha256", a = "host date request-line", h = `host: ${t}
11
- date: ${r}
12
- GET /v2/iat HTTP/1.1`, c = d.HmacSHA256(h, s), l = d.enc.Base64.stringify(c), g = `api_key="${e}", algorithm="${n}", headers="${a}", signature="${l}"`, f = window.btoa(g);
13
- return `${i}?authorization=${f}&date=${r}&host=${t}`;
14
- }
15
- class C {
16
- constructor(t) {
17
- o(this, "wsInstance", null);
18
- o(this, "status", "UNDEFINED");
19
- o(this, "recorder", null);
20
- o(this, "isDestroy", !1);
21
- o(this, "resultText", "");
22
- o(this, "resultTextTemp", "");
23
- o(this, "continueRecord", !1);
24
- o(this, "startTimeStamp", 0);
25
- o(this, "recordStatus", "stop");
26
- o(this, "readyToSend", !1);
27
- this.config = t, this.initializeRecorder();
28
- }
29
- renderResult(t) {
30
- var s, r;
31
- const e = JSON.parse(t);
32
- if (e.data && e.data.result) {
33
- const n = e.data.result;
34
- let a = "";
35
- const h = n.ws;
36
- for (let c = 0; c < h.length; c++)
37
- a = a + h[c].cw[0].w;
38
- n.pgs ? (n.pgs === "apd" && (this.resultText = this.resultTextTemp), this.resultTextTemp = this.resultText + a) : this.resultText = this.resultText + a;
39
- }
40
- if (e.code === 0 && e.data.status === 2) {
41
- this.config.onMessage({
42
- message: this.resultText,
43
- isLatest: !0,
44
- tempMessage: this.resultTextTemp
45
- }), (s = this.wsInstance) == null || s.close();
46
- return;
47
- }
48
- e.code === 0 && e.data.status === 1 && this.config.onMessage({
49
- message: this.resultText,
50
- isLatest: !1,
51
- tempMessage: this.resultTextTemp
52
- }), e.code !== 0 && ((r = this.wsInstance) == null || r.close(), this.config.onError({ type: "message", message: t }));
53
- }
54
- setConfig(t) {
55
- this.config = {
56
- ...this.config,
57
- ...t
58
- }, this.initializeRecorder();
59
- }
60
- sendMessage(t) {
61
- var e;
62
- ((e = this.wsInstance) == null ? void 0 : e.readyState) === WebSocket.OPEN && this.readyToSend && this.wsInstance.send(t);
63
- }
64
- async recordConfigSend() {
65
- var t;
66
- if (!this.isDestroy)
67
- try {
68
- await ((t = this.recorder) == null ? void 0 : t.start({
69
- sampleRate: 16e3,
70
- frameSize: 1280
71
- }));
72
- const e = {
73
- common: {
74
- app_id: T
75
- },
76
- business: {
77
- language: "zh_cn",
78
- domain: "iat",
79
- accent: "mandarin",
80
- vad_eos: 5e3,
81
- dwa: "wpgs"
82
- },
83
- data: {
84
- status: 0,
85
- format: "audio/L16;rate=16000",
86
- encoding: "raw"
87
- }
88
- };
89
- this.readyToSend = !0, this.sendMessage(JSON.stringify(e));
90
- } catch (e) {
91
- console.error("record error:", e), this.config.onError({ type: "record", message: "录音失败, 请检查权限!" });
92
- }
93
- }
94
- startNewSocket() {
95
- var e, s;
96
- console.log(
97
- "%c ❤️ love ==== start new socket:",
98
- "color: red; font-size: 16px;",
99
- u,
100
- (/* @__PURE__ */ new Date()).toUTCString()
101
- ), u++;
102
- const t = ((s = (e = this.config).getWebSocketUrl) == null ? void 0 : s.call(e)) || R();
103
- if ("WebSocket" in window)
104
- this.wsInstance = new WebSocket(t);
105
- else {
106
- this.config.onError({ type: "socketConnect", message: "浏览器不支持 WebSocket" });
107
- return;
108
- }
109
- this.wsInstance.onmessage = (r) => {
110
- this.renderResult(r.data);
111
- }, this.wsInstance.onerror = (r) => {
112
- console.error("socket error:", r), this.changeBtnStatus("CLOSED");
113
- }, this.wsInstance.onclose = async () => {
114
- var r, n;
115
- this.changeBtnStatus("CLOSED"), this.config.onWsClose(), (r = this.recorder) == null || r.stop(), this.continueRecord && (this.readyToSend = !1, await ((n = this.recorder) == null ? void 0 : n.close()), this.startRecord());
116
- }, this.wsInstance.onopen = () => {
117
- this.changeBtnStatus("OPEN"), this.recordConfigSend();
118
- };
119
- }
120
- recordProcess({ isLastFrame: t, frameBuffer: e }) {
121
- this.isDestroy || this.wsInstance && (this.wsInstance.readyState === this.wsInstance.OPEN && this.sendMessage(
122
- JSON.stringify({
123
- data: {
124
- status: t ? 2 : 1,
125
- format: "audio/L16;rate=16000",
126
- encoding: "raw",
127
- audio: p(e)
128
- }
129
- })
130
- ), t && (this.readyToSend = !1, this.changeBtnStatus("CLOSING")));
131
- }
132
- initializeRecorder() {
133
- this.recorder = new w(), this.recorder && (this.recorder.onFrameRecorded = (t) => {
134
- this.recordProcess(t);
135
- }, this.recorder.onStop = (t) => {
136
- var e, s;
137
- (s = (e = this.config).onRecordStop) == null || s.call(e, t);
138
- }, this.recorder.onStart = () => {
139
- var t, e;
140
- console.log("%c ❤️ love ==== recorder onStart:", "color: red; font-size: 16px;"), (e = (t = this.config).onRecordStart) == null || e.call(t), this.changeBtnStatus("OPEN");
141
- });
142
- }
143
- changeBtnStatus(t) {
144
- this.status = t;
145
- }
146
- startRecord() {
147
- this.isDestroy || (this.startTimeStamp = Date.now(), this.resultText = "", this.resultTextTemp = "", this.startNewSocket());
148
- }
149
- async stopRecord() {
150
- var t;
151
- console.log("stop record ====", Date.now() - this.startTimeStamp), Date.now() - this.startTimeStamp < 500 ? setTimeout(() => {
152
- var e;
153
- (e = this.recorder) == null || e.stop();
154
- }, 100) : (t = this.recorder) == null || t.stop(), setTimeout(() => {
155
- var e, s;
156
- ((e = this.wsInstance) == null ? void 0 : e.readyState) === WebSocket.OPEN && ((s = this.wsInstance) == null || s.close());
157
- }, 5e3);
158
- }
159
- /** 开启持续监听 */
160
- startContinueRecord() {
161
- this.continueRecord = !0, this.startRecord();
162
- }
163
- stopContinueRecord() {
164
- this.continueRecord = !1, this.stopRecord();
165
- }
166
- destroy() {
167
- this.isDestroy = !0, setTimeout(() => {
168
- var t, e, s;
169
- ((t = this.wsInstance) == null ? void 0 : t.readyState) === WebSocket.OPEN && ((e = this.wsInstance) == null || e.close()), (s = this.recorder) == null || s.stop();
170
- }, 1e3);
171
- }
172
- }
173
- export {
174
- C as XunFeiStreamVoiceManager
175
- };
176
- //# sourceMappingURL=XunFeiStreamVoiceManager.es.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"XunFeiStreamVoiceManager.es.js","sources":["../../../src/utils/xunFeiVoice/XunFeiStreamVoiceManager.ts"],"sourcesContent":["import CryptoJS from 'crypto-js';\nimport { RecorderManager } from './RecorderManager';\nimport type { OnMessageInfo, RecordFrameInfo } from './types';\nimport { toBase64 } from './utils';\n\ntype errorType = 'socketConnect' | 'message' | 'record';\nlet startCount = 0;\n\nexport interface XunFeiStreamVoiceManagerConfig {\n onError: (errInfo: { type: errorType; message: string }) => void;\n /** ws 收到消息的回调\n * @param info.message 消息内容\n * @param info.isLatest 是否是最后一条消息。 isLatest 为 true 时, message 为完整的语音识别结果。\n */\n onMessage: (info: OnMessageInfo) => void;\n /** webSocket 关闭的回调。\n * 每次录音都会新建一个 webSocket 连接。录音结束后关闭 webSocket\n */\n onWsClose: () => void;\n /**\n * 录音开始的回调\n */\n onRecordStart?: () => void;\n /**\n * 录音结束的回调\n */\n onRecordStop?: (data: ArrayBuffer[]) => void;\n /**\n * 获取链接讯飞语音识别地址的 url\n * @returns websocket url。详情查看讯飞文档里的鉴权方法:https://www.xfyun.cn/doc/asr/voicedictation/API.html#%E6%8E%A5%E5%8F%A3%E8%B0%83%E7%94%A8%E6%B5%81%E7%A8%8B\n */\n getWebSocketUrl?: () => string;\n}\ntype SocketStatus = 'UNDEFINED' | 'CONNECTING' | 'OPEN' | 'CLOSING' | 'CLOSED';\n\n/** 现在前端mock,后续移到接口下发 */\nconst APPID = '93b73e33';\nconst API_SECRET = 'ZGJhMzQ5ZTJlMDgyYmE1ZTFlZDlmYjg0';\nconst API_KEY = 'fe43de7a071e3a32ec03c6f09fe7bedf';\n/**\n * 获取websocket url\n * 该接口需要后端提供,这里为了方便前端处理\n */\nfunction getWebSocketUrl() {\n // 请求地址根据语种不同变化\n const url = 'wss://iat-api.xfyun.cn/v2/iat';\n const host = 'iat-api.xfyun.cn';\n const apiKey = API_KEY;\n const apiSecret = API_SECRET;\n const date = new Date().toUTCString();\n const algorithm = 'hmac-sha256';\n const headers = 'host date request-line';\n const signatureOrigin = `host: ${host}\\ndate: ${date}\\nGET /v2/iat HTTP/1.1`;\n const signatureSha = CryptoJS.HmacSHA256(signatureOrigin, apiSecret);\n const signature = CryptoJS.enc.Base64.stringify(signatureSha);\n const authorizationOrigin = `api_key=\"${apiKey}\", algorithm=\"${algorithm}\", headers=\"${headers}\", signature=\"${signature}\"`;\n const authorization = window.btoa(authorizationOrigin);\n return `${url}?authorization=${authorization}&date=${date}&host=${host}`;\n}\n\n/**\n * 讯飞流式语音识别\n */\nexport class XunFeiStreamVoiceManager {\n wsInstance: WebSocket | null = null;\n status: SocketStatus = 'UNDEFINED';\n recorder: RecorderManager | null = null;\n isDestroy = false;\n resultText = '';\n resultTextTemp = '';\n continueRecord = false;\n startTimeStamp = 0;\n recordStatus: 'start' | 'stop' = 'stop';\n readyToSend = false;\n\n constructor(public config: XunFeiStreamVoiceManagerConfig) {\n this.initializeRecorder();\n }\n renderResult(resultData: string) {\n // 识别结束\n const jsonData = JSON.parse(resultData);\n if (jsonData.data && jsonData.data.result) {\n const data = jsonData.data.result;\n let str = '';\n const ws = data.ws;\n for (let i = 0; i < ws.length; i++) {\n str = str + ws[i].cw[0].w;\n }\n // 开启 wpgs 会有此字段(前提:在控制台开通动态修正功能)\n // 取值为 \"apd\"时表示该片结果是追加到前面的最终结果;取值为\"rpl\" 时表示替换前面的部分结果,替换范围为rg字段\n if (data.pgs) {\n if (data.pgs === 'apd') {\n // 将resultTextTemp同步给resultText\n this.resultText = this.resultTextTemp;\n }\n // 将结果存储在resultTextTemp中\n this.resultTextTemp = this.resultText + str;\n } else {\n this.resultText = this.resultText + str;\n }\n }\n /**\n * 识别结果是否结束标识:\n * 0:识别的第一块结果。\n * 1:识别中间结果。\n * 2:识别最后一块结果\n */\n if (jsonData.code === 0 && jsonData.data.status === 2) {\n this.config.onMessage({\n message: this.resultText,\n isLatest: true,\n tempMessage: this.resultTextTemp,\n });\n this.wsInstance?.close();\n return;\n }\n if (jsonData.code === 0 && jsonData.data.status === 1) {\n this.config.onMessage({\n message: this.resultText,\n isLatest: false,\n tempMessage: this.resultTextTemp,\n });\n }\n // code 不为0时表示出错\n if (jsonData.code !== 0) {\n this.wsInstance?.close();\n this.config.onError({ type: 'message', message: resultData });\n }\n }\n\n setConfig(newConfig: XunFeiStreamVoiceManagerConfig) {\n this.config = {\n ...this.config,\n ...newConfig,\n };\n this.initializeRecorder();\n }\n sendMessage(message: string) {\n if (this.wsInstance?.readyState === WebSocket.OPEN && this.readyToSend) {\n this.wsInstance.send(message);\n }\n }\n\n async recordConfigSend() {\n if (this.isDestroy) {\n return;\n }\n try {\n // 开始录音\n await this.recorder?.start({\n sampleRate: 16000,\n frameSize: 1280,\n });\n const params = {\n common: {\n app_id: APPID,\n },\n business: {\n language: 'zh_cn',\n domain: 'iat',\n accent: 'mandarin',\n vad_eos: 5000,\n dwa: 'wpgs',\n },\n data: {\n status: 0,\n format: 'audio/L16;rate=16000',\n encoding: 'raw',\n },\n };\n this.readyToSend = true;\n this.sendMessage(JSON.stringify(params));\n } catch (error) {\n console.error('record error:', error);\n this.config.onError({ type: 'record', message: '录音失败, 请检查权限!' });\n }\n }\n\n startNewSocket() {\n console.log(\n '%c ❤️ love ==== start new socket:',\n 'color: red; font-size: 16px;',\n startCount,\n new Date().toUTCString(),\n );\n startCount++;\n const websocketUrl = this.config.getWebSocketUrl?.() || getWebSocketUrl();\n if ('WebSocket' in window) {\n this.wsInstance = new WebSocket(websocketUrl);\n } else {\n this.config.onError({ type: 'socketConnect', message: '浏览器不支持 WebSocket' });\n return;\n }\n this.wsInstance.onmessage = (e) => {\n // 处理返回数据\n this.renderResult(e.data);\n };\n this.wsInstance.onerror = (e) => {\n console.error('socket error:', e);\n this.changeBtnStatus('CLOSED');\n };\n this.wsInstance.onclose = async () => {\n this.changeBtnStatus('CLOSED');\n this.config.onWsClose();\n this.recorder?.stop();\n if (this.continueRecord) {\n this.readyToSend = false;\n await this.recorder?.close();\n this.startRecord();\n }\n };\n this.wsInstance.onopen = () => {\n this.changeBtnStatus('OPEN');\n this.recordConfigSend();\n };\n }\n\n recordProcess({ isLastFrame, frameBuffer }: RecordFrameInfo) {\n if (this.isDestroy) {\n return;\n }\n if (!this.wsInstance) {\n return;\n }\n if (this.wsInstance.readyState === this.wsInstance.OPEN) {\n this.sendMessage(\n JSON.stringify({\n data: {\n status: isLastFrame ? 2 : 1,\n format: 'audio/L16;rate=16000',\n encoding: 'raw',\n audio: toBase64(frameBuffer),\n },\n }),\n );\n }\n if (isLastFrame) {\n this.readyToSend = false;\n this.changeBtnStatus('CLOSING');\n }\n }\n\n initializeRecorder() {\n this.recorder = new RecorderManager();\n if (!this.recorder) {\n return;\n }\n this.recorder.onFrameRecorded = (data: RecordFrameInfo) => {\n this.recordProcess(data);\n };\n this.recorder.onStop = (data: ArrayBuffer[]) => {\n this.config.onRecordStop?.(data);\n };\n this.recorder.onStart = () => {\n console.log('%c ❤️ love ==== recorder onStart:', 'color: red; font-size: 16px;');\n this.config.onRecordStart?.();\n this.changeBtnStatus('OPEN');\n };\n }\n\n changeBtnStatus(status: SocketStatus) {\n this.status = status;\n }\n\n startRecord() {\n if (this.isDestroy) {\n return;\n }\n this.startTimeStamp = Date.now();\n this.resultText = '';\n this.resultTextTemp = '';\n this.startNewSocket();\n }\n\n async stopRecord() {\n console.log('stop record ====', Date.now() - this.startTimeStamp);\n // 结束录音,为了防止刚开始就关闭\n if (Date.now() - this.startTimeStamp < 500) {\n setTimeout(() => {\n this.recorder?.stop();\n }, 100);\n } else {\n this.recorder?.stop();\n }\n // 这里为了防止ws没有返回结束的数据,导致 ws 一直没关,所以加个定时器,5s 后手动关闭\n setTimeout(() => {\n if (this.wsInstance?.readyState === WebSocket.OPEN) {\n this.wsInstance?.close();\n }\n }, 5000);\n }\n\n /** 开启持续监听 */\n startContinueRecord() {\n this.continueRecord = true;\n this.startRecord();\n }\n\n stopContinueRecord() {\n this.continueRecord = false;\n this.stopRecord();\n }\n\n destroy() {\n this.isDestroy = true;\n setTimeout(() => {\n if (this.wsInstance?.readyState === WebSocket.OPEN) {\n this.wsInstance?.close();\n }\n this.recorder?.stop();\n }, 1000);\n }\n}\n"],"names":["startCount","APPID","API_SECRET","API_KEY","getWebSocketUrl","url","host","apiKey","apiSecret","date","algorithm","headers","signatureOrigin","signatureSha","CryptoJS","signature","authorizationOrigin","authorization","XunFeiStreamVoiceManager","config","__publicField","resultData","jsonData","data","str","ws","i","_a","_b","newConfig","message","params","error","websocketUrl","e","isLastFrame","frameBuffer","toBase64","RecorderManager","status","_c"],"mappings":"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"}