@mingxy/ocosay 1.1.32 → 1.2.0
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/dist/core/backends/index.js +41 -17
- package/dist/core/backends/playsound-backend.d.ts +1 -1
- package/dist/core/backends/playsound-backend.js +85 -51
- package/dist/core/backends/speaker-backend.js +2 -1
- package/dist/core/notification.d.ts +7 -1
- package/dist/core/notification.js +64 -11
- package/dist/core/player.js +2 -5
- package/dist/core/stream-player.d.ts +3 -2
- package/dist/core/stream-player.js +21 -9
- package/dist/index.js +2 -2
- package/dist/package.json +1 -1
- package/dist/plugin.js +311 -153
- package/dist/providers/minimax.d.ts +0 -1
- package/dist/providers/minimax.js +0 -1
- package/dist/services/notification-service.d.ts +13 -4
- package/dist/services/notification-service.js +61 -11
- package/dist/services/speaker-service.d.ts +1 -2
- package/dist/services/speaker-service.js +1 -3
- package/dist/services/streaming-service.js +3 -3
- package/package.json +1 -1
|
@@ -18,6 +18,7 @@ import { HowlerBackend } from './howler-backend';
|
|
|
18
18
|
import { PlaySoundBackend } from './playsound-backend';
|
|
19
19
|
import { SpeakerBackend } from './speaker-backend';
|
|
20
20
|
import { logger } from '../../utils/logger';
|
|
21
|
+
import { notificationService } from '../notification';
|
|
21
22
|
function execCmd(cmd) {
|
|
22
23
|
try {
|
|
23
24
|
const output = execSync(cmd, { stdio: 'pipe', encoding: 'utf8' });
|
|
@@ -44,21 +45,6 @@ export var BackendType;
|
|
|
44
45
|
BackendType["SPEAKER"] = "speaker";
|
|
45
46
|
BackendType["AUTO"] = "auto";
|
|
46
47
|
})(BackendType || (BackendType = {}));
|
|
47
|
-
let naudiodonCache = null;
|
|
48
|
-
async function tryLoadNaudiodon() {
|
|
49
|
-
if (naudiodonCache !== null) {
|
|
50
|
-
return naudiodonCache;
|
|
51
|
-
}
|
|
52
|
-
try {
|
|
53
|
-
naudiodonCache = await import('naudiodon');
|
|
54
|
-
return naudiodonCache;
|
|
55
|
-
}
|
|
56
|
-
catch (err) {
|
|
57
|
-
logger.warn({ err }, 'failed to load naudiodon module');
|
|
58
|
-
naudiodonCache = false;
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
48
|
function isNaudiodonAvailable() {
|
|
63
49
|
try {
|
|
64
50
|
require.resolve('naudiodon');
|
|
@@ -79,12 +65,27 @@ function isSpeakerAvailable() {
|
|
|
79
65
|
return false;
|
|
80
66
|
}
|
|
81
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* 检测是否运行在 WSL (Windows Subsystem for Linux) 环境中
|
|
70
|
+
*/
|
|
71
|
+
function isWSL() {
|
|
72
|
+
if (process.platform !== 'linux')
|
|
73
|
+
return false;
|
|
74
|
+
try {
|
|
75
|
+
const output = execSync('uname -r', { stdio: 'pipe', encoding: 'utf8' });
|
|
76
|
+
return output.toLowerCase().includes('microsoft');
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
82
|
export function createBackend(type = BackendType.AUTO, options = {}) {
|
|
83
83
|
const platform = process.platform;
|
|
84
84
|
if (type !== BackendType.AUTO) {
|
|
85
85
|
return createBackendByType(type, options);
|
|
86
86
|
}
|
|
87
|
-
|
|
87
|
+
// WSL 环境下 naudiodon 可能无法工作(无法访问 Windows 音频设备),跳过
|
|
88
|
+
if (!isWSL() && isNaudiodonAvailable()) {
|
|
88
89
|
try {
|
|
89
90
|
const naudiodon = require('naudiodon');
|
|
90
91
|
if (naudiodon) {
|
|
@@ -97,13 +98,35 @@ export function createBackend(type = BackendType.AUTO, options = {}) {
|
|
|
97
98
|
}
|
|
98
99
|
catch (err) {
|
|
99
100
|
logger.error({ err }, 'failed to initialize naudiodon backend');
|
|
101
|
+
notificationService.warning('naudiodon 初始化失败', '将使用其他音频后端', 5000);
|
|
100
102
|
}
|
|
101
103
|
}
|
|
102
104
|
switch (platform) {
|
|
103
105
|
case 'darwin':
|
|
104
106
|
return new AfplayBackend(options);
|
|
105
|
-
case 'linux':
|
|
107
|
+
case 'linux': {
|
|
108
|
+
const wsl = isWSL();
|
|
109
|
+
if (wsl) {
|
|
110
|
+
logger.debug('Running on WSL, skipping naudiodon (may not work with Windows audio)');
|
|
111
|
+
}
|
|
106
112
|
// Linux 环境检测顺序:naudiodon → aplay → play-sound → speaker → Howler
|
|
113
|
+
// WSL 环境下 naudiodon 可能无法工作,直接尝试其他后端
|
|
114
|
+
if (!wsl && isNaudiodonAvailable()) {
|
|
115
|
+
try {
|
|
116
|
+
const naudiodon = require('naudiodon');
|
|
117
|
+
if (naudiodon) {
|
|
118
|
+
const devices = naudiodon.getDevices();
|
|
119
|
+
if (devices && devices.length > 0) {
|
|
120
|
+
return new NaudiodonBackend(options);
|
|
121
|
+
}
|
|
122
|
+
logger.debug('naudiodon has no audio devices, skipping');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
logger.error({ err }, 'failed to initialize naudiodon backend');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// aplay 后端
|
|
107
130
|
if (isCommandAvailable('aplay')) {
|
|
108
131
|
const test = execCmd('aplay -l');
|
|
109
132
|
if (test.success && !test.output.includes('no soundcards')) {
|
|
@@ -121,6 +144,7 @@ export function createBackend(type = BackendType.AUTO, options = {}) {
|
|
|
121
144
|
// 彻底失败,使用 Howler 作为最后的回退
|
|
122
145
|
logger.warn('All Linux audio backends failed, using HowlerBackend as fallback');
|
|
123
146
|
return new HowlerBackend(options);
|
|
147
|
+
}
|
|
124
148
|
case 'win32':
|
|
125
149
|
return new PowerShellBackend(options);
|
|
126
150
|
default:
|
|
@@ -24,7 +24,7 @@ export declare class PlaySoundBackend implements AudioBackend {
|
|
|
24
24
|
private chunks;
|
|
25
25
|
private hasEnded;
|
|
26
26
|
constructor(options?: BackendOptions);
|
|
27
|
-
start(filePath: string): void
|
|
27
|
+
start(filePath: string): Promise<void>;
|
|
28
28
|
private playWithPlaySound;
|
|
29
29
|
write(chunk: Buffer): void;
|
|
30
30
|
end(): void;
|
|
@@ -7,6 +7,8 @@ import { execFile } from 'child_process';
|
|
|
7
7
|
import { tmpdir } from 'os';
|
|
8
8
|
import { join } from 'path';
|
|
9
9
|
import { writeFileSync, unlinkSync, existsSync } from 'fs';
|
|
10
|
+
import { promisify } from 'util';
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
10
12
|
// 白名单:只允许特定路径格式(禁止 - 防止命令注入)
|
|
11
13
|
const SAFE_PATH_REGEX = /^[\w\/\.]+$/;
|
|
12
14
|
/**
|
|
@@ -32,7 +34,7 @@ export class PlaySoundBackend {
|
|
|
32
34
|
constructor(options = {}) {
|
|
33
35
|
this.events = options.events;
|
|
34
36
|
}
|
|
35
|
-
start(filePath) {
|
|
37
|
+
async start(filePath) {
|
|
36
38
|
if (this._started)
|
|
37
39
|
return;
|
|
38
40
|
if (!SAFE_PATH_REGEX.test(filePath)) {
|
|
@@ -42,62 +44,94 @@ export class PlaySoundBackend {
|
|
|
42
44
|
this._started = true;
|
|
43
45
|
this._stopped = false;
|
|
44
46
|
this.events?.onStart?.();
|
|
45
|
-
//
|
|
46
|
-
this.playWithPlaySound(filePath);
|
|
47
|
+
// 等待 play-sound 播放完成
|
|
48
|
+
await this.playWithPlaySound(filePath);
|
|
47
49
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
playWithPlaySound(filePath) {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
50
52
|
// 异步导入 play-sound
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
53
|
+
import('play-sound').then((playModule) => {
|
|
54
|
+
const play = playModule.default;
|
|
55
|
+
// 对于 ffplay,使用无声模式
|
|
56
|
+
if (process.platform === 'linux') {
|
|
57
|
+
// ffplay 无声卡播放参数
|
|
58
|
+
execFileAsync('ffplay', [
|
|
59
|
+
'-nodisp', // 不显示窗口
|
|
60
|
+
'-autoexit', // 播放完自动退出
|
|
61
|
+
'-loglevel', 'error', // 减少日志输出
|
|
62
|
+
filePath
|
|
63
|
+
]).then(() => {
|
|
64
|
+
if (this._stopped) {
|
|
65
|
+
resolve();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
this._started = false;
|
|
69
|
+
this.events?.onEnd?.();
|
|
70
|
+
resolve();
|
|
71
|
+
}).catch((error) => {
|
|
72
|
+
if (this._stopped) {
|
|
73
|
+
resolve();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
this._started = false;
|
|
68
77
|
this.handleError(error);
|
|
69
|
-
|
|
78
|
+
reject(error);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
// 使用 play-sound 的 Promise API
|
|
83
|
+
// play-sound 的类型定义不完善,需要用 any 访问 .play() 方法
|
|
84
|
+
const playerSound = play;
|
|
85
|
+
const p = playerSound.play(filePath);
|
|
86
|
+
if (p && p.then) {
|
|
87
|
+
// 返回 Promise 的情况
|
|
88
|
+
p.then(() => {
|
|
89
|
+
if (this._stopped) {
|
|
90
|
+
resolve();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
this._started = false;
|
|
94
|
+
this.events?.onEnd?.();
|
|
95
|
+
resolve();
|
|
96
|
+
}).catch((err) => {
|
|
97
|
+
if (this._stopped) {
|
|
98
|
+
resolve();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
this._started = false;
|
|
102
|
+
this.handleError(err);
|
|
103
|
+
reject(err);
|
|
104
|
+
});
|
|
70
105
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
106
|
+
else if (p && p.kill) {
|
|
107
|
+
// 返回 ChildProcess 的情况
|
|
108
|
+
this.player = p;
|
|
109
|
+
p.on('error', (error) => {
|
|
110
|
+
this.handleError(error);
|
|
111
|
+
reject(error);
|
|
112
|
+
});
|
|
113
|
+
p.on('close', () => {
|
|
114
|
+
if (this._stopped) {
|
|
115
|
+
resolve();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
this._started = false;
|
|
119
|
+
this.events?.onEnd?.();
|
|
120
|
+
resolve();
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
// 无法识别的返回类型,直接 resolve
|
|
125
|
+
resolve();
|
|
84
126
|
}
|
|
85
|
-
this._started = false;
|
|
86
|
-
this.events?.onEnd?.();
|
|
87
|
-
});
|
|
88
|
-
if (p && p.kill) {
|
|
89
|
-
this.player = p;
|
|
90
127
|
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
catch (err) {
|
|
99
|
-
this.handleError(err instanceof Error ? err : new Error(String(err)));
|
|
100
|
-
}
|
|
128
|
+
}).catch((err) => {
|
|
129
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
130
|
+
error.message = `play-sound load failed: ${error.message}`;
|
|
131
|
+
this.handleError(error);
|
|
132
|
+
reject(error);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
101
135
|
}
|
|
102
136
|
write(chunk) {
|
|
103
137
|
if (this._stopped)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type ToastVariant = 'success' | 'error' | 'info' | 'warning';
|
|
2
2
|
export interface ToastOptions {
|
|
3
|
-
title
|
|
3
|
+
title?: string;
|
|
4
4
|
message: string;
|
|
5
5
|
variant?: ToastVariant;
|
|
6
6
|
duration?: number;
|
|
@@ -17,10 +17,16 @@ declare class NotificationService {
|
|
|
17
17
|
showToast(options: ToastOptions): boolean;
|
|
18
18
|
private scheduleRetry;
|
|
19
19
|
private flushPending;
|
|
20
|
+
success(message: string, duration?: number): boolean;
|
|
20
21
|
success(title: string, message: string, duration?: number): boolean;
|
|
22
|
+
error(message: string, duration?: number): boolean;
|
|
21
23
|
error(title: string, message: string, duration?: number): boolean;
|
|
24
|
+
info(message: string, duration?: number): boolean;
|
|
22
25
|
info(title: string, message: string, duration?: number): boolean;
|
|
26
|
+
warning(message: string, duration?: number): boolean;
|
|
23
27
|
warning(title: string, message: string, duration?: number): boolean;
|
|
28
|
+
showSpinnerToast(title: string, message: string, duration?: number): Promise<void>;
|
|
29
|
+
private getTitleForVariant;
|
|
24
30
|
}
|
|
25
31
|
export declare const notificationService: NotificationService;
|
|
26
32
|
export default notificationService;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/core/notification.ts
|
|
2
2
|
import { createModuleLogger } from '../utils/logger';
|
|
3
3
|
const logger = createModuleLogger('NotificationService');
|
|
4
|
+
const SISYPHUS_SPINNER = ['·', '•', '●', '○', '◌', '◦', ' '];
|
|
4
5
|
/**
|
|
5
6
|
* NotificationService - 统一 Toast 通知管理
|
|
6
7
|
* 参照 DCP 实现,不做防御性检查,直接调用并用 try-catch 处理
|
|
@@ -15,10 +16,11 @@ class NotificationService {
|
|
|
15
16
|
this.flushPending();
|
|
16
17
|
}
|
|
17
18
|
showToast(options) {
|
|
18
|
-
const
|
|
19
|
+
const title = options.title || this.getTitleForVariant(options.variant || 'info');
|
|
20
|
+
const { message, variant = 'info', duration = 5000 } = options;
|
|
19
21
|
if (!this.tui) {
|
|
20
22
|
logger.debug({ title }, 'tui not ready, queueing toast');
|
|
21
|
-
this.pendingToasts.push(options);
|
|
23
|
+
this.pendingToasts.push({ ...options, title });
|
|
22
24
|
this.scheduleRetry();
|
|
23
25
|
return false;
|
|
24
26
|
}
|
|
@@ -38,7 +40,7 @@ class NotificationService {
|
|
|
38
40
|
catch (err) {
|
|
39
41
|
// 参照 DCP:捕获异常但不抛出
|
|
40
42
|
logger.warn({ err, title }, 'toast call failed, queueing for retry');
|
|
41
|
-
this.pendingToasts.push(options);
|
|
43
|
+
this.pendingToasts.push({ ...options, title });
|
|
42
44
|
this.scheduleRetry();
|
|
43
45
|
return false;
|
|
44
46
|
}
|
|
@@ -68,17 +70,68 @@ class NotificationService {
|
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
72
|
}
|
|
71
|
-
success(
|
|
72
|
-
|
|
73
|
+
success(titleOrMessage, messageOrDuration, duration) {
|
|
74
|
+
if (typeof messageOrDuration === 'string') {
|
|
75
|
+
// 3参数: success(title, message, duration)
|
|
76
|
+
return this.showToast({ title: titleOrMessage, message: messageOrDuration, variant: 'success', duration });
|
|
77
|
+
}
|
|
78
|
+
// 2参数: success(message, duration) 或 success(message)
|
|
79
|
+
return this.showToast({ message: titleOrMessage, variant: 'success', duration: messageOrDuration });
|
|
73
80
|
}
|
|
74
|
-
error(
|
|
75
|
-
|
|
81
|
+
error(titleOrMessage, messageOrDuration, duration) {
|
|
82
|
+
if (typeof messageOrDuration === 'string') {
|
|
83
|
+
return this.showToast({ title: titleOrMessage, message: messageOrDuration, variant: 'error', duration });
|
|
84
|
+
}
|
|
85
|
+
return this.showToast({ message: titleOrMessage, variant: 'error', duration: messageOrDuration || 8000 });
|
|
76
86
|
}
|
|
77
|
-
info(
|
|
78
|
-
|
|
87
|
+
info(titleOrMessage, messageOrDuration, duration) {
|
|
88
|
+
if (typeof messageOrDuration === 'string') {
|
|
89
|
+
return this.showToast({ title: titleOrMessage, message: messageOrDuration, variant: 'info', duration });
|
|
90
|
+
}
|
|
91
|
+
return this.showToast({ message: titleOrMessage, variant: 'info', duration: messageOrDuration });
|
|
92
|
+
}
|
|
93
|
+
warning(titleOrMessage, messageOrDuration, duration) {
|
|
94
|
+
if (typeof messageOrDuration === 'string') {
|
|
95
|
+
return this.showToast({ title: titleOrMessage, message: messageOrDuration, variant: 'warning', duration });
|
|
96
|
+
}
|
|
97
|
+
return this.showToast({ message: titleOrMessage, variant: 'warning', duration: messageOrDuration });
|
|
98
|
+
}
|
|
99
|
+
async showSpinnerToast(title, message, duration = 2000) {
|
|
100
|
+
const frameInterval = 100;
|
|
101
|
+
const totalFrames = Math.ceil(duration / frameInterval);
|
|
102
|
+
for (let i = 0; i < totalFrames; i++) {
|
|
103
|
+
const spinner = SISYPHUS_SPINNER[i % SISYPHUS_SPINNER.length];
|
|
104
|
+
const toastDuration = Math.min(frameInterval + 50, duration - i * frameInterval);
|
|
105
|
+
if (toastDuration <= 0)
|
|
106
|
+
break;
|
|
107
|
+
if (this.tui?.showToast) {
|
|
108
|
+
try {
|
|
109
|
+
await this.tui.showToast({
|
|
110
|
+
body: {
|
|
111
|
+
title: `${spinner} ${title}`,
|
|
112
|
+
message,
|
|
113
|
+
variant: 'info',
|
|
114
|
+
duration: toastDuration
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
logger.warn({ err }, 'showSpinnerToast failed');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (i < totalFrames - 1) {
|
|
123
|
+
await new Promise((resolve) => setTimeout(resolve, frameInterval));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
79
126
|
}
|
|
80
|
-
|
|
81
|
-
|
|
127
|
+
getTitleForVariant(variant) {
|
|
128
|
+
const titles = {
|
|
129
|
+
success: 'Success',
|
|
130
|
+
error: 'Error',
|
|
131
|
+
warning: 'Warning',
|
|
132
|
+
info: 'Info'
|
|
133
|
+
};
|
|
134
|
+
return titles[variant];
|
|
82
135
|
}
|
|
83
136
|
}
|
|
84
137
|
export const notificationService = new NotificationService();
|
package/dist/core/player.js
CHANGED
|
@@ -120,11 +120,8 @@ export class AudioPlayer extends EventEmitter {
|
|
|
120
120
|
* 播放音频文件
|
|
121
121
|
* 使用 AudioBackend 统一后端播放
|
|
122
122
|
*/
|
|
123
|
-
playFile(filePath, _format) {
|
|
124
|
-
|
|
125
|
-
this.backend.start(filePath);
|
|
126
|
-
resolve(); // backend.start() 是同步的,立即 resolve
|
|
127
|
-
});
|
|
123
|
+
async playFile(filePath, _format) {
|
|
124
|
+
await Promise.resolve(this.backend.start(filePath));
|
|
128
125
|
}
|
|
129
126
|
pause() {
|
|
130
127
|
if (!this._playing || this._paused)
|
|
@@ -38,6 +38,7 @@ export declare class StreamPlayer extends EventEmitter {
|
|
|
38
38
|
private _started;
|
|
39
39
|
private _paused;
|
|
40
40
|
private _stopped;
|
|
41
|
+
private _starting;
|
|
41
42
|
private format;
|
|
42
43
|
private events?;
|
|
43
44
|
constructor(options?: StreamPlayerOptions);
|
|
@@ -45,12 +46,12 @@ export declare class StreamPlayer extends EventEmitter {
|
|
|
45
46
|
* 开始播放
|
|
46
47
|
* 初始化后端,准备接收音频数据
|
|
47
48
|
*/
|
|
48
|
-
start(): void
|
|
49
|
+
start(): Promise<void>;
|
|
49
50
|
/**
|
|
50
51
|
* 写入音频数据块(边收边播)
|
|
51
52
|
* 如果尚未 start(),会自动调用
|
|
52
53
|
*/
|
|
53
|
-
write(chunk: Buffer): void
|
|
54
|
+
write(chunk: Buffer): Promise<void>;
|
|
54
55
|
/**
|
|
55
56
|
* 结束写入
|
|
56
57
|
* 通知后端写入完成,但保持播放直到结束
|
|
@@ -18,14 +18,15 @@ export class StreamPlayer extends EventEmitter {
|
|
|
18
18
|
_started = false;
|
|
19
19
|
_paused = false;
|
|
20
20
|
_stopped = false;
|
|
21
|
+
_starting = false;
|
|
21
22
|
format = 'mp3';
|
|
22
23
|
events;
|
|
23
24
|
constructor(options = {}) {
|
|
24
25
|
super();
|
|
25
26
|
this.format = options.format || 'mp3';
|
|
26
27
|
this.events = options.events;
|
|
27
|
-
//
|
|
28
|
-
const backendType = options.backendType || BackendType.
|
|
28
|
+
// 创建音频后端,默认使用 AUTO 自动选择合适的后端
|
|
29
|
+
const backendType = options.backendType || BackendType.AUTO;
|
|
29
30
|
this.backend = createBackend(backendType, {
|
|
30
31
|
format: this.format,
|
|
31
32
|
events: {
|
|
@@ -65,7 +66,7 @@ export class StreamPlayer extends EventEmitter {
|
|
|
65
66
|
* 开始播放
|
|
66
67
|
* 初始化后端,准备接收音频数据
|
|
67
68
|
*/
|
|
68
|
-
start() {
|
|
69
|
+
async start() {
|
|
69
70
|
if (this._started) {
|
|
70
71
|
return;
|
|
71
72
|
}
|
|
@@ -73,8 +74,8 @@ export class StreamPlayer extends EventEmitter {
|
|
|
73
74
|
this.handleError(new Error('Audio backend not initialized'));
|
|
74
75
|
return;
|
|
75
76
|
}
|
|
76
|
-
//
|
|
77
|
-
this.backend.start('');
|
|
77
|
+
// 初始化后端(playsound-backend 是异步的,需 await)
|
|
78
|
+
await Promise.resolve(this.backend.start(''));
|
|
78
79
|
this._started = true;
|
|
79
80
|
this._stopped = false;
|
|
80
81
|
this._paused = false;
|
|
@@ -84,18 +85,29 @@ export class StreamPlayer extends EventEmitter {
|
|
|
84
85
|
* 写入音频数据块(边收边播)
|
|
85
86
|
* 如果尚未 start(),会自动调用
|
|
86
87
|
*/
|
|
87
|
-
write(chunk) {
|
|
88
|
+
async write(chunk) {
|
|
88
89
|
// 如果已停止,直接忽略
|
|
89
90
|
if (this._stopped) {
|
|
90
91
|
return;
|
|
91
92
|
}
|
|
92
|
-
//
|
|
93
|
+
// 如果未启动,防止竞态条件并自动启动
|
|
93
94
|
if (!this._started) {
|
|
94
|
-
|
|
95
|
+
// 防止并发启动
|
|
96
|
+
while (this._starting) {
|
|
97
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
98
|
+
}
|
|
99
|
+
this._starting = true;
|
|
100
|
+
try {
|
|
101
|
+
await this.start();
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
this._starting = false;
|
|
105
|
+
}
|
|
95
106
|
}
|
|
96
107
|
// 写入数据到后端
|
|
97
108
|
if (this.backend) {
|
|
98
|
-
|
|
109
|
+
// backend.write() 可能返回 Promise 或 void,用 Promise.resolve 处理
|
|
110
|
+
await Promise.resolve(this.backend.write(chunk));
|
|
99
111
|
this._bytesWritten += chunk.length;
|
|
100
112
|
this.events?.onProgress?.(this._bytesWritten);
|
|
101
113
|
this.emit('progress', this._bytesWritten);
|
package/dist/index.js
CHANGED
|
@@ -65,7 +65,7 @@ function initializeStreamComponents(config) {
|
|
|
65
65
|
const playerEvents = {
|
|
66
66
|
onStart: () => { },
|
|
67
67
|
onEnd: () => { },
|
|
68
|
-
onProgress: (
|
|
68
|
+
onProgress: (_bytesWritten) => { },
|
|
69
69
|
onError: (error) => logger.error({ error }, 'stream player error'),
|
|
70
70
|
onStop: () => { }
|
|
71
71
|
};
|
|
@@ -161,7 +161,7 @@ export async function destroy() {
|
|
|
161
161
|
streamingSynthesizer = undefined;
|
|
162
162
|
}
|
|
163
163
|
if (streamPlayer) {
|
|
164
|
-
|
|
164
|
+
streamPlayer.stop();
|
|
165
165
|
streamPlayer = undefined;
|
|
166
166
|
}
|
|
167
167
|
if (speaker) {
|