@idlebox/browser 0.0.45 → 0.0.46
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.
- package/lib/binray/download-blob.d.ts +2 -0
- package/lib/binray/download-blob.d.ts.map +1 -0
- package/lib/binray/download-blob.js +9 -0
- package/lib/binray/download-blob.js.map +1 -0
- package/lib/storage/{timeoutStorage.d.ts → with-timeout.d.ts} +4 -1
- package/lib/storage/with-timeout.d.ts.map +1 -0
- package/lib/storage/{timeoutStorage.js → with-timeout.js} +4 -1
- package/lib/storage/with-timeout.js.map +1 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/web-audio/audio-player.d.ts +44 -0
- package/lib/web-audio/audio-player.d.ts.map +1 -0
- package/lib/web-audio/audio-player.js +242 -0
- package/lib/web-audio/audio-player.js.map +1 -0
- package/lib/web-audio/pcm-recorder.d.ts +65 -0
- package/lib/web-audio/pcm-recorder.d.ts.map +1 -0
- package/lib/web-audio/pcm-recorder.js +226 -0
- package/lib/web-audio/pcm-recorder.js.map +1 -0
- package/package.json +5 -6
- package/src/binray/download-blob.ts +8 -0
- package/src/global.d.ts +10 -0
- package/src/storage/{timeoutStorage.ts → with-timeout.ts} +3 -0
- package/src/web-audio/audio-player.ts +278 -0
- package/src/web-audio/pcm-recorder.ts +281 -0
- package/lib/autoindex.generated.d.ts +0 -3
- package/lib/autoindex.generated.d.ts.map +0 -1
- package/lib/autoindex.generated.js +0 -10
- package/lib/autoindex.generated.js.map +0 -1
- package/lib/storage/timeoutStorage.d.ts.map +0 -1
- package/lib/storage/timeoutStorage.js.map +0 -1
- package/src/autoindex.generated.ts +0 -10
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { closableToDisposable, DeferredPromise, definePublicConstant, DuplicateDisposeAction, Emitter, EnhancedDisposable, ExtendableTimer, sleep, } from '@idlebox/common';
|
|
2
|
+
/**
|
|
3
|
+
* TODO: need a logger
|
|
4
|
+
*/
|
|
5
|
+
const logger = console;
|
|
6
|
+
/**
|
|
7
|
+
* TODO: 新api要求使用webworker 这里模拟worker
|
|
8
|
+
*/
|
|
9
|
+
export class RawPcmStreamNode extends EnhancedDisposable {
|
|
10
|
+
audioContext;
|
|
11
|
+
bitDepth;
|
|
12
|
+
latency;
|
|
13
|
+
duplicateDispose = DuplicateDisposeAction.Allow;
|
|
14
|
+
_onDataAvailable = new Emitter();
|
|
15
|
+
onDataAvailable = this._onDataAvailable.event;
|
|
16
|
+
/**
|
|
17
|
+
* 由于AudioNode没有类似end的事件,只能用延迟模拟一个
|
|
18
|
+
*/
|
|
19
|
+
_onFinished = new Emitter();
|
|
20
|
+
onFinished = this._onFinished.event;
|
|
21
|
+
/**
|
|
22
|
+
* 被要求结束后,只有连续没有收到音频,才真正触发 onFinished
|
|
23
|
+
* 这和决定录音何时结束无关
|
|
24
|
+
*/
|
|
25
|
+
willFinish;
|
|
26
|
+
_node;
|
|
27
|
+
constructor(audioContext, bitDepth = 16, latency = 150) {
|
|
28
|
+
super();
|
|
29
|
+
this.audioContext = audioContext;
|
|
30
|
+
this.bitDepth = bitDepth;
|
|
31
|
+
this.latency = latency;
|
|
32
|
+
this.willFinish = new ExtendableTimer(latency * 2);
|
|
33
|
+
this.willFinish.onSchedule(() => {
|
|
34
|
+
logger.debug('连续没有收到音频,录制正确结束');
|
|
35
|
+
this._onFinished.fireNoError();
|
|
36
|
+
this.dispose();
|
|
37
|
+
});
|
|
38
|
+
this.onPostDispose(() => {
|
|
39
|
+
this._onFinished.dispose();
|
|
40
|
+
if (this._node) {
|
|
41
|
+
this._node.disconnect();
|
|
42
|
+
this._node = undefined;
|
|
43
|
+
}
|
|
44
|
+
this.willFinish.cancel();
|
|
45
|
+
this._onDataAvailable.dispose();
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
get bufferSize() {
|
|
49
|
+
return calculateBufferSize(this.latency, this.audioContext.sampleRate, 1, this.bitDepth);
|
|
50
|
+
}
|
|
51
|
+
_getNode() {
|
|
52
|
+
if (!this._node) {
|
|
53
|
+
this._node = this.audioContext.createScriptProcessor(this.bufferSize, 1, 1);
|
|
54
|
+
logger.debug(`创建新的stream-node: buffer size = %s`, this.bufferSize);
|
|
55
|
+
}
|
|
56
|
+
this._node.onaudioprocess = (event) => {
|
|
57
|
+
this.willFinish.renew();
|
|
58
|
+
const rawPcmData = event.inputBuffer.getChannelData(0);
|
|
59
|
+
logger.debug('~ script process data %s frames', event.inputBuffer.length);
|
|
60
|
+
const buff = float32_sint16(rawPcmData);
|
|
61
|
+
this._onDataAvailable.fireNoError(new Uint8Array(buff.buffer));
|
|
62
|
+
};
|
|
63
|
+
// 实际无用,但没有目标就不产生事件
|
|
64
|
+
this._node.connect(this.audioContext.destination);
|
|
65
|
+
return this._node;
|
|
66
|
+
}
|
|
67
|
+
connectFrom(source) {
|
|
68
|
+
source.connect(this._getNode());
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* 外部不要调用
|
|
72
|
+
*
|
|
73
|
+
* 优雅结束
|
|
74
|
+
* @private
|
|
75
|
+
*/
|
|
76
|
+
shutdown() {
|
|
77
|
+
logger.debug('即将结束录制过程');
|
|
78
|
+
this.willFinish.start();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export class RawPcmStreamRecorder extends EnhancedDisposable {
|
|
82
|
+
bitDepth;
|
|
83
|
+
sampleRate;
|
|
84
|
+
duplicateDispose = DuplicateDisposeAction.Allow;
|
|
85
|
+
channels = 1;
|
|
86
|
+
dfd = new DeferredPromise();
|
|
87
|
+
constructor(bitDepth = 16, sampleRate = 16000) {
|
|
88
|
+
super();
|
|
89
|
+
this.bitDepth = bitDepth;
|
|
90
|
+
this.sampleRate = sampleRate;
|
|
91
|
+
}
|
|
92
|
+
get context() {
|
|
93
|
+
logger.debug('creating new audio context');
|
|
94
|
+
const context = new AudioContext({ sampleRate: this.sampleRate });
|
|
95
|
+
context.addEventListener('statechange', () => {
|
|
96
|
+
if (context.state === 'closed' && !this.hasDisposed) {
|
|
97
|
+
this.dispose();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
definePublicConstant(this, 'context', context);
|
|
101
|
+
this._register(closableToDisposable(context));
|
|
102
|
+
this.onBeforeDispose(() => {
|
|
103
|
+
if (!this.dfd.settled)
|
|
104
|
+
this.dfd.error(new Error('录制操作中断'));
|
|
105
|
+
this.started = undefined;
|
|
106
|
+
this.close_microphone(0);
|
|
107
|
+
});
|
|
108
|
+
return context;
|
|
109
|
+
}
|
|
110
|
+
started;
|
|
111
|
+
/**
|
|
112
|
+
* 开始录音(可以重复调用,返回相同)
|
|
113
|
+
* stop之后不能重新start
|
|
114
|
+
*/
|
|
115
|
+
startRecording(latency = 150) {
|
|
116
|
+
if (this.disposed || this.hasClosed) {
|
|
117
|
+
throw new Error('recorder is already finished');
|
|
118
|
+
}
|
|
119
|
+
if (!this.started) {
|
|
120
|
+
this.started = this._startRecording(latency);
|
|
121
|
+
}
|
|
122
|
+
return this.started;
|
|
123
|
+
}
|
|
124
|
+
_microphone;
|
|
125
|
+
_analyze;
|
|
126
|
+
_recorder;
|
|
127
|
+
async _startRecording(latency) {
|
|
128
|
+
logger.debug('启动录音,缓冲 %sms', latency);
|
|
129
|
+
const microphone = await navigator.mediaDevices
|
|
130
|
+
.getUserMedia({
|
|
131
|
+
video: false,
|
|
132
|
+
audio: {
|
|
133
|
+
sampleRate: this.sampleRate,
|
|
134
|
+
channelCount: this.channels,
|
|
135
|
+
sampleSize: this.bitDepth,
|
|
136
|
+
echoCancellation: true,
|
|
137
|
+
noiseSuppression: true,
|
|
138
|
+
autoGainControl: true,
|
|
139
|
+
},
|
|
140
|
+
})
|
|
141
|
+
.catch((e) => {
|
|
142
|
+
this.dfd.error(e);
|
|
143
|
+
throw e;
|
|
144
|
+
});
|
|
145
|
+
const streamNode = this.context.createMediaStreamSource(microphone);
|
|
146
|
+
for (const item of microphone.getAudioTracks()) {
|
|
147
|
+
item.addEventListener('ended', () => {
|
|
148
|
+
logger.warn('麦克风由于外部原因停止');
|
|
149
|
+
this.close_microphone(0);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
this._microphone = microphone;
|
|
153
|
+
const recorder = new RawPcmStreamNode(this.context, this.bitDepth, latency);
|
|
154
|
+
this._recorder = recorder;
|
|
155
|
+
recorder.connectFrom(streamNode);
|
|
156
|
+
recorder.onFinished(() => {
|
|
157
|
+
this.dfd.complete();
|
|
158
|
+
});
|
|
159
|
+
const analyze = this.context.createAnalyser();
|
|
160
|
+
this._analyze = analyze;
|
|
161
|
+
streamNode.connect(analyze);
|
|
162
|
+
return recorder;
|
|
163
|
+
}
|
|
164
|
+
caclculateVolume() {
|
|
165
|
+
if (!this._analyze)
|
|
166
|
+
return 0;
|
|
167
|
+
const bufferLength = this._analyze.frequencyBinCount;
|
|
168
|
+
const dataArray = new Uint8Array(bufferLength);
|
|
169
|
+
this._analyze.getByteFrequencyData(dataArray);
|
|
170
|
+
let sum = 0;
|
|
171
|
+
for (const amplitude of dataArray) {
|
|
172
|
+
sum += amplitude * amplitude;
|
|
173
|
+
}
|
|
174
|
+
return Math.sqrt(sum / dataArray.length);
|
|
175
|
+
}
|
|
176
|
+
getPromise() {
|
|
177
|
+
return this.dfd.p;
|
|
178
|
+
}
|
|
179
|
+
hasClosed = false;
|
|
180
|
+
close_microphone(delay) {
|
|
181
|
+
if (this.hasClosed)
|
|
182
|
+
return;
|
|
183
|
+
this.hasClosed = true;
|
|
184
|
+
logger.debug('程序主动关闭麦克风 | 延迟 %sms', delay);
|
|
185
|
+
sleep(delay).then(() => {
|
|
186
|
+
if (this._microphone) {
|
|
187
|
+
// 其实只有一个
|
|
188
|
+
for (const item of this._microphone.getAudioTracks()) {
|
|
189
|
+
item.stop();
|
|
190
|
+
this._microphone.removeTrack(item);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (this._recorder)
|
|
194
|
+
this._recorder.shutdown();
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
async stopRecording() {
|
|
198
|
+
const volume = this.caclculateVolume();
|
|
199
|
+
// 不知道是否合适
|
|
200
|
+
const delay = (1000 * volume) / 60;
|
|
201
|
+
this.close_microphone(delay);
|
|
202
|
+
return new Promise((resolve, reject) => {
|
|
203
|
+
if (!this._recorder)
|
|
204
|
+
return resolve();
|
|
205
|
+
this._recorder.onBeforeDispose(resolve);
|
|
206
|
+
this._recorder.onBeforeDispose(() => reject(new Error('录制操作非正常中断')));
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function calculateBufferSize(milliseconds, sampleRate, channels, bits) {
|
|
211
|
+
const rawSize = Math.floor((milliseconds / 1000) * sampleRate * channels * (bits / 8));
|
|
212
|
+
// 返回大于rawSize的最接近的2的幂
|
|
213
|
+
const near = 2 ** Math.ceil(Math.log2(rawSize));
|
|
214
|
+
return Math.max(256, Math.min(16384, near));
|
|
215
|
+
}
|
|
216
|
+
function float32_sint16(input) {
|
|
217
|
+
// 将Float32Array转换为Int16Array
|
|
218
|
+
const output = new Int16Array(input.length);
|
|
219
|
+
for (let i = 0; i < input.length; i++) {
|
|
220
|
+
let s = input[i];
|
|
221
|
+
s = Math.max(-1, Math.min(1, s));
|
|
222
|
+
output[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
|
|
223
|
+
}
|
|
224
|
+
return output;
|
|
225
|
+
}
|
|
226
|
+
//# sourceMappingURL=pcm-recorder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pcm-recorder.js","sourceRoot":"","sources":["../../src/web-audio/pcm-recorder.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idlebox/browser",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.46",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
7
7
|
"default": "./lib/autoindex.generated.js"
|
|
@@ -10,14 +10,13 @@
|
|
|
10
10
|
},
|
|
11
11
|
"sideEffects": false,
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@idlebox/common": "^1.4.
|
|
14
|
-
"@idlebox/source-map-support": "^0.0.
|
|
13
|
+
"@idlebox/common": "^1.4.21",
|
|
14
|
+
"@idlebox/source-map-support": "^0.0.8"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"@rushstack/heft": "^0.74.0",
|
|
18
17
|
"@build-script/single-dog-asset": "^1.0.39",
|
|
19
|
-
"@
|
|
20
|
-
"@
|
|
18
|
+
"@mpis/run": "^0.0.9",
|
|
19
|
+
"@internal/local-rig": "^1.0.1"
|
|
21
20
|
},
|
|
22
21
|
"license": "MIT",
|
|
23
22
|
"author": "GongT <admin@gongt.me>",
|
package/src/global.d.ts
CHANGED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { DeferredPromise, Emitter, type IDisposable } from '@idlebox/common';
|
|
2
|
+
|
|
3
|
+
const DELAY_CLOSE_MOUTH = 700;
|
|
4
|
+
|
|
5
|
+
export class StreamAppender {
|
|
6
|
+
private readonly dfd = new DeferredPromise<void>();
|
|
7
|
+
private readonly queue: ArrayBuffer[] = [];
|
|
8
|
+
private _finished = false;
|
|
9
|
+
|
|
10
|
+
constructor(private readonly stream: SourceBuffer) {
|
|
11
|
+
stream.mode = 'sequence';
|
|
12
|
+
|
|
13
|
+
stream.addEventListener('updateend', this._pump.bind(this));
|
|
14
|
+
stream.addEventListener('error', this._error.bind(this));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private _pump() {
|
|
18
|
+
// console.log('[stream] pump %s', this.queue.length);
|
|
19
|
+
if (this.stream.updating) {
|
|
20
|
+
// console.error(' - busy');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const next = this.queue.shift();
|
|
24
|
+
if (next) {
|
|
25
|
+
// console.log('playing %s bytes', next.byteLength);
|
|
26
|
+
this.stream.appendBuffer(next);
|
|
27
|
+
} else if (this._finished) {
|
|
28
|
+
// console.log('StreamAppender: dispose');
|
|
29
|
+
this.dfd.complete();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private _error(e: Event) {
|
|
34
|
+
// console.log('[stream] pump fail', e);
|
|
35
|
+
this.queue.length = 0;
|
|
36
|
+
this.finish();
|
|
37
|
+
|
|
38
|
+
const err = (e as any).error;
|
|
39
|
+
if (!(err instanceof Error)) {
|
|
40
|
+
console.error('e似乎不是ErrorEvent', e);
|
|
41
|
+
}
|
|
42
|
+
this.dfd.error(err);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
append(buffer: ArrayBuffer) {
|
|
46
|
+
if (this._finished) {
|
|
47
|
+
throw new Error('不能在finish之后append');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.queue.push(buffer);
|
|
51
|
+
if (!this.stream.updating) {
|
|
52
|
+
// console.log('[stream] pump start');
|
|
53
|
+
this._pump();
|
|
54
|
+
} else {
|
|
55
|
+
// console.log('[stream] queue data');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
finish() {
|
|
60
|
+
// console.log('[stream] done.');
|
|
61
|
+
this._finished = true;
|
|
62
|
+
this._pump();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
terminate() {
|
|
66
|
+
// console.log('[stream] done. (terminate)');
|
|
67
|
+
this._finished = true;
|
|
68
|
+
this.queue.length = 0;
|
|
69
|
+
this._pump();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
wait(): Promise<void> {
|
|
73
|
+
return this.dfd.p;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class MediaForPlayback {
|
|
78
|
+
private readonly mediaSource: MediaSource;
|
|
79
|
+
public readonly ready: Promise<void>;
|
|
80
|
+
private readonly endDfd = new DeferredPromise<void>();
|
|
81
|
+
|
|
82
|
+
public readonly id: number;
|
|
83
|
+
|
|
84
|
+
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: bug
|
|
85
|
+
private static guid = 0;
|
|
86
|
+
|
|
87
|
+
constructor() {
|
|
88
|
+
this.id = ++MediaForPlayback.guid;
|
|
89
|
+
// console.log('[media source %d] create', this.id);
|
|
90
|
+
|
|
91
|
+
const source = new window.MediaSource();
|
|
92
|
+
this.ready = new Promise<void>((resolve) => {
|
|
93
|
+
const wrap = () => {
|
|
94
|
+
source.removeEventListener('sourceopen', wrap);
|
|
95
|
+
// console.log('[media source %d] sourceopen', this.id);
|
|
96
|
+
resolve();
|
|
97
|
+
};
|
|
98
|
+
source.addEventListener('sourceopen', wrap);
|
|
99
|
+
});
|
|
100
|
+
this.mediaSource = source;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private opened = false;
|
|
104
|
+
async open(mime: string) {
|
|
105
|
+
if (this.opened) throw new Error('duplicate call to MediaForPlayback.open()');
|
|
106
|
+
this.opened = true;
|
|
107
|
+
|
|
108
|
+
// console.log('[media source %d] open', this.id, mime);
|
|
109
|
+
await this.ready;
|
|
110
|
+
|
|
111
|
+
const buffer = this.mediaSource.addSourceBuffer(mime);
|
|
112
|
+
const appender = new StreamAppender(buffer);
|
|
113
|
+
appender.wait().finally(() => {
|
|
114
|
+
// console.log('[media source %d] appender finished', this.id);
|
|
115
|
+
|
|
116
|
+
this.mediaSource.endOfStream();
|
|
117
|
+
this.endDfd.complete();
|
|
118
|
+
this.dispose();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return appender;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
playToNewAudioElement() {
|
|
125
|
+
const audio = new HtmlAudioPlayer();
|
|
126
|
+
|
|
127
|
+
audio.onEnd(() => {
|
|
128
|
+
this.dispose();
|
|
129
|
+
});
|
|
130
|
+
audio.element.src = URL.createObjectURL(this.mediaSource);
|
|
131
|
+
|
|
132
|
+
return audio;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
public finish(): Promise<void> {
|
|
136
|
+
return this.endDfd.p;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private disList: IDisposable[] = [];
|
|
140
|
+
_register(d: IDisposable) {
|
|
141
|
+
this.disList.push(d);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
dispose() {
|
|
145
|
+
// console.log('[media source %d] dispose()', this.id);
|
|
146
|
+
for (const obj of this.disList) {
|
|
147
|
+
obj.dispose();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export class HtmlAudioPlayer {
|
|
153
|
+
public readonly element: HTMLAudioElement;
|
|
154
|
+
|
|
155
|
+
private readonly _humanSpeaking = new Emitter<boolean>();
|
|
156
|
+
public readonly onHumanSpeaking = this._humanSpeaking.event;
|
|
157
|
+
|
|
158
|
+
constructor() {
|
|
159
|
+
const audio = new Audio();
|
|
160
|
+
this.element = audio;
|
|
161
|
+
|
|
162
|
+
// for (const n of mdevents) {
|
|
163
|
+
// audio.addEventListener(n, () => {
|
|
164
|
+
// console.log('[audio] event: %s', n);
|
|
165
|
+
// });
|
|
166
|
+
// }
|
|
167
|
+
|
|
168
|
+
audio.addEventListener('ended', () => {
|
|
169
|
+
this.dispose();
|
|
170
|
+
});
|
|
171
|
+
audio.addEventListener('canplay', () => {
|
|
172
|
+
this.aboutToSetSpeaking(true);
|
|
173
|
+
});
|
|
174
|
+
audio.addEventListener('pause', () => {
|
|
175
|
+
this.aboutToSetSpeaking(false);
|
|
176
|
+
});
|
|
177
|
+
audio.addEventListener('waiting', () => {
|
|
178
|
+
this.aboutToSetSpeaking(false, DELAY_CLOSE_MOUTH / 2);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
audio.autoplay = true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private tmr?: number;
|
|
185
|
+
private aboutToSetSpeaking(target: boolean, timeout = DELAY_CLOSE_MOUTH) {
|
|
186
|
+
if (this.tmr) {
|
|
187
|
+
clearTimeout(this.tmr);
|
|
188
|
+
this.tmr = 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (target) {
|
|
192
|
+
this._moveSpeakState(true);
|
|
193
|
+
} else {
|
|
194
|
+
this.tmr = setTimeout(() => {
|
|
195
|
+
this.tmr = 0;
|
|
196
|
+
|
|
197
|
+
this._moveSpeakState(false);
|
|
198
|
+
}, timeout);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private speaking = false;
|
|
203
|
+
private _moveSpeakState(target: boolean) {
|
|
204
|
+
if (this.speaking === target) return;
|
|
205
|
+
|
|
206
|
+
this.speaking = target;
|
|
207
|
+
// console.log('human-speaking:', target);
|
|
208
|
+
this._humanSpeaking.fireNoError(this.speaking);
|
|
209
|
+
|
|
210
|
+
if (this.disposed) {
|
|
211
|
+
this._humanSpeaking.dispose();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private disposed = false;
|
|
216
|
+
dispose() {
|
|
217
|
+
// console.log('HtmlAudioPlayer: dispose');
|
|
218
|
+
this.disposed = true;
|
|
219
|
+
|
|
220
|
+
disposeAudioElement(this.element);
|
|
221
|
+
|
|
222
|
+
if (this.speaking) {
|
|
223
|
+
this.aboutToSetSpeaking(false);
|
|
224
|
+
} else {
|
|
225
|
+
this._humanSpeaking.dispose();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
onEnd(fn: () => void) {
|
|
230
|
+
if (this.element.ended) {
|
|
231
|
+
fn();
|
|
232
|
+
} else {
|
|
233
|
+
const once = () => {
|
|
234
|
+
fn();
|
|
235
|
+
this.element.removeEventListener('ended', once);
|
|
236
|
+
};
|
|
237
|
+
this.element.addEventListener('ended', once);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function disposeAudioElement(audio: HTMLAudioElement) {
|
|
243
|
+
if (!audio.ended && !('__my_end' in audio)) {
|
|
244
|
+
Object.assign(audio, { __my_end: true });
|
|
245
|
+
const ee = new Event('ended');
|
|
246
|
+
Object.assign(ee, { error: new Error('canceled') });
|
|
247
|
+
audio.dispatchEvent(ee);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// @ts-ignore
|
|
252
|
+
// biome-ignore lint/correctness/noUnusedVariables: debug
|
|
253
|
+
const mdevents = [
|
|
254
|
+
'abort',
|
|
255
|
+
'canplay',
|
|
256
|
+
'canplaythrough',
|
|
257
|
+
'durationchange',
|
|
258
|
+
'emptied',
|
|
259
|
+
'encrypted',
|
|
260
|
+
'ended',
|
|
261
|
+
'error',
|
|
262
|
+
'loadeddata',
|
|
263
|
+
'loadedmetadata',
|
|
264
|
+
'loadstart',
|
|
265
|
+
'pause',
|
|
266
|
+
'play',
|
|
267
|
+
'playing',
|
|
268
|
+
'progress',
|
|
269
|
+
'ratechange',
|
|
270
|
+
'seeked',
|
|
271
|
+
'seeking',
|
|
272
|
+
'stalled',
|
|
273
|
+
'suspend',
|
|
274
|
+
'timeupdate',
|
|
275
|
+
'volumechange',
|
|
276
|
+
'waiting',
|
|
277
|
+
'waitingforkey',
|
|
278
|
+
];
|