@mingxy/ocosay 1.0.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.
Files changed (126) hide show
  1. package/README.md +556 -0
  2. package/TECH_PLAN.md +352 -0
  3. package/__mocks__/@opencode-ai/plugin.ts +32 -0
  4. package/dist/config.d.ts +26 -0
  5. package/dist/config.d.ts.map +1 -0
  6. package/dist/config.js +95 -0
  7. package/dist/config.js.map +1 -0
  8. package/dist/core/backends/afplay-backend.d.ts +33 -0
  9. package/dist/core/backends/afplay-backend.d.ts.map +1 -0
  10. package/dist/core/backends/afplay-backend.js +144 -0
  11. package/dist/core/backends/afplay-backend.js.map +1 -0
  12. package/dist/core/backends/aplay-backend.d.ts +33 -0
  13. package/dist/core/backends/aplay-backend.d.ts.map +1 -0
  14. package/dist/core/backends/aplay-backend.js +142 -0
  15. package/dist/core/backends/aplay-backend.js.map +1 -0
  16. package/dist/core/backends/base.d.ts +94 -0
  17. package/dist/core/backends/base.d.ts.map +1 -0
  18. package/dist/core/backends/base.js +6 -0
  19. package/dist/core/backends/base.js.map +1 -0
  20. package/dist/core/backends/index.d.ts +29 -0
  21. package/dist/core/backends/index.d.ts.map +1 -0
  22. package/dist/core/backends/index.js +114 -0
  23. package/dist/core/backends/index.js.map +1 -0
  24. package/dist/core/backends/naudiodon-backend.d.ts +52 -0
  25. package/dist/core/backends/naudiodon-backend.d.ts.map +1 -0
  26. package/dist/core/backends/naudiodon-backend.js +123 -0
  27. package/dist/core/backends/naudiodon-backend.js.map +1 -0
  28. package/dist/core/backends/powershell-backend.d.ts +34 -0
  29. package/dist/core/backends/powershell-backend.d.ts.map +1 -0
  30. package/dist/core/backends/powershell-backend.js +154 -0
  31. package/dist/core/backends/powershell-backend.js.map +1 -0
  32. package/dist/core/player.d.ts +97 -0
  33. package/dist/core/player.d.ts.map +1 -0
  34. package/dist/core/player.js +268 -0
  35. package/dist/core/player.js.map +1 -0
  36. package/dist/core/speaker.d.ts +97 -0
  37. package/dist/core/speaker.d.ts.map +1 -0
  38. package/dist/core/speaker.js +218 -0
  39. package/dist/core/speaker.js.map +1 -0
  40. package/dist/core/stream-player.d.ts +107 -0
  41. package/dist/core/stream-player.d.ts.map +1 -0
  42. package/dist/core/stream-player.js +272 -0
  43. package/dist/core/stream-player.js.map +1 -0
  44. package/dist/core/stream-reader.d.ts +86 -0
  45. package/dist/core/stream-reader.d.ts.map +1 -0
  46. package/dist/core/stream-reader.js +172 -0
  47. package/dist/core/stream-reader.js.map +1 -0
  48. package/dist/core/streaming-synthesizer.d.ts +51 -0
  49. package/dist/core/streaming-synthesizer.d.ts.map +1 -0
  50. package/dist/core/streaming-synthesizer.js +103 -0
  51. package/dist/core/streaming-synthesizer.js.map +1 -0
  52. package/dist/core/types.d.ts +141 -0
  53. package/dist/core/types.d.ts.map +1 -0
  54. package/dist/core/types.js +37 -0
  55. package/dist/core/types.js.map +1 -0
  56. package/dist/index.d.ts +40 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +179 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/plugin.d.ts +4 -0
  61. package/dist/plugin.d.ts.map +1 -0
  62. package/dist/plugin.js +151 -0
  63. package/dist/plugin.js.map +1 -0
  64. package/dist/providers/base.d.ts +55 -0
  65. package/dist/providers/base.d.ts.map +1 -0
  66. package/dist/providers/base.js +95 -0
  67. package/dist/providers/base.js.map +1 -0
  68. package/dist/providers/minimax.d.ts +84 -0
  69. package/dist/providers/minimax.d.ts.map +1 -0
  70. package/dist/providers/minimax.js +387 -0
  71. package/dist/providers/minimax.js.map +1 -0
  72. package/dist/tools/tts.d.ts +147 -0
  73. package/dist/tools/tts.d.ts.map +1 -0
  74. package/dist/tools/tts.js +232 -0
  75. package/dist/tools/tts.js.map +1 -0
  76. package/jest.config.js +15 -0
  77. package/package.json +49 -0
  78. package/src/config.ts +121 -0
  79. package/src/core/backends/afplay-backend.ts +162 -0
  80. package/src/core/backends/aplay-backend.ts +160 -0
  81. package/src/core/backends/base.ts +117 -0
  82. package/src/core/backends/index.ts +128 -0
  83. package/src/core/backends/naudiodon-backend.ts +164 -0
  84. package/src/core/backends/powershell-backend.ts +173 -0
  85. package/src/core/player.ts +322 -0
  86. package/src/core/speaker.ts +283 -0
  87. package/src/core/stream-player.ts +326 -0
  88. package/src/core/stream-reader.ts +190 -0
  89. package/src/core/streaming-synthesizer.ts +123 -0
  90. package/src/core/types.ts +185 -0
  91. package/src/index.ts +233 -0
  92. package/src/plugin.ts +166 -0
  93. package/src/providers/base.ts +150 -0
  94. package/src/providers/minimax.ts +515 -0
  95. package/src/tools/tts.ts +277 -0
  96. package/src/types/naudiodon.d.ts +19 -0
  97. package/tests/__mocks__/@opencode-ai/plugin.ts +32 -0
  98. package/tests/backends.test.ts +831 -0
  99. package/tests/index.test.ts +201 -0
  100. package/tests/integration-test.d.ts +6 -0
  101. package/tests/integration-test.d.ts.map +1 -0
  102. package/tests/integration-test.js +84 -0
  103. package/tests/integration-test.js.map +1 -0
  104. package/tests/integration-test.ts +93 -0
  105. package/tests/p1-fixes.test.ts +160 -0
  106. package/tests/plugin.test.ts +311 -0
  107. package/tests/provider.test.d.ts +2 -0
  108. package/tests/provider.test.d.ts.map +1 -0
  109. package/tests/provider.test.js +69 -0
  110. package/tests/provider.test.js.map +1 -0
  111. package/tests/provider.test.ts +87 -0
  112. package/tests/speaker.test.d.ts +2 -0
  113. package/tests/speaker.test.d.ts.map +1 -0
  114. package/tests/speaker.test.js +63 -0
  115. package/tests/speaker.test.js.map +1 -0
  116. package/tests/speaker.test.ts +232 -0
  117. package/tests/stream-player.test.ts +303 -0
  118. package/tests/stream-reader.test.ts +269 -0
  119. package/tests/streaming-synthesizer.test.ts +225 -0
  120. package/tests/tts-tools.test.ts +270 -0
  121. package/tests/types.test.d.ts +2 -0
  122. package/tests/types.test.d.ts.map +1 -0
  123. package/tests/types.test.js +61 -0
  124. package/tests/types.test.js.map +1 -0
  125. package/tests/types.test.ts +63 -0
  126. package/tsconfig.json +22 -0
@@ -0,0 +1,283 @@
1
+ /**
2
+ * Speaker - TTS 统一调用入口
3
+ * 提供简洁的 API 和便捷函数
4
+ */
5
+
6
+ import { EventEmitter } from 'events'
7
+ import {
8
+ TTSProvider,
9
+ TTSError,
10
+ TTSErrorCode,
11
+ SpeakOptions,
12
+ AudioResult,
13
+ Voice,
14
+ TTSEvent,
15
+ SynthesisModel
16
+ } from './types'
17
+ import { getProvider, listProviders, hasProvider } from '../providers/base'
18
+ import { AudioPlayer, PlayerEvents } from './player'
19
+
20
+ export interface SpeakerOptions {
21
+ defaultProvider?: string
22
+ defaultModel?: SynthesisModel
23
+ defaultVoice?: string
24
+ onEvent?: (event: TTSEvent, data?: any) => void
25
+ }
26
+
27
+ /**
28
+ * Speaker - TTS 统一调用入口类
29
+ * 封装 Provider 和 Player,提供简洁的 speak/pause/resume/stop API
30
+ */
31
+ export class Speaker extends EventEmitter {
32
+ private currentProvider?: TTSProvider
33
+ private player?: AudioPlayer
34
+ private currentText?: string
35
+ private isSpeaking = false
36
+ private isPaused = false
37
+
38
+ constructor(private options: SpeakerOptions = {}) {
39
+ super()
40
+
41
+ // 初始化播放器
42
+ const playerEvents: PlayerEvents = {
43
+ onStart: () => this.emit('start', this.currentText),
44
+ onEnd: () => {
45
+ this.isSpeaking = false
46
+ this.emit('end', this.currentText)
47
+ },
48
+ onError: (error) => this.emit('error', error),
49
+ onPause: () => {
50
+ this.isPaused = true
51
+ this.emit('pause')
52
+ },
53
+ onResume: () => {
54
+ this.isPaused = false
55
+ this.emit('resume')
56
+ },
57
+ onStop: () => {
58
+ this.isSpeaking = false
59
+ this.isPaused = false
60
+ this.emit('stop')
61
+ }
62
+ }
63
+
64
+ this.player = new AudioPlayer(playerEvents)
65
+ }
66
+
67
+ /**
68
+ * 说话 - 核心方法
69
+ * @param text 要说的文本
70
+ * @param options 可选参数
71
+ */
72
+ async speak(
73
+ text: string,
74
+ options: SpeakOptions & { provider?: string } = {}
75
+ ): Promise<void> {
76
+ // 参数校验
77
+ if (!text || text.trim().length === 0) {
78
+ throw new TTSError(
79
+ 'Text cannot be empty',
80
+ TTSErrorCode.INVALID_PARAMS,
81
+ 'speaker'
82
+ )
83
+ }
84
+
85
+ // 停止当前播放
86
+ if (this.isSpeaking) {
87
+ await this.stop()
88
+ }
89
+
90
+ this.isSpeaking = true
91
+ this.currentText = text
92
+
93
+ try {
94
+ // 获取 provider
95
+ const providerName = options.provider || this.options.defaultProvider || 'minimax'
96
+ if (!hasProvider(providerName)) {
97
+ throw new TTSError(
98
+ `Provider "${providerName}" not found`,
99
+ TTSErrorCode.UNKNOWN,
100
+ 'speaker'
101
+ )
102
+ }
103
+
104
+ this.currentProvider = getProvider(providerName)
105
+
106
+ // 调用 provider 生成音频
107
+ const result = await this.currentProvider.speak(text, {
108
+ model: options.model || this.options.defaultModel || 'stream',
109
+ voice: options.voice || this.options.defaultVoice,
110
+ speed: options.speed,
111
+ volume: options.volume,
112
+ pitch: options.pitch,
113
+ sourceVoice: options.sourceVoice
114
+ })
115
+
116
+ // 播放音频
117
+ if (this.player) {
118
+ await this.player.play(result.audioData, result.format)
119
+ }
120
+
121
+ } catch (error) {
122
+ this.isSpeaking = false
123
+ if (error instanceof TTSError) {
124
+ this.emit('error', error)
125
+ throw error
126
+ }
127
+ const ttsError = new TTSError(
128
+ 'Speak failed',
129
+ TTSErrorCode.UNKNOWN,
130
+ 'speaker',
131
+ error
132
+ )
133
+ this.emit('error', ttsError)
134
+ throw ttsError
135
+ }
136
+ }
137
+
138
+ /**
139
+ * 暂停播放
140
+ */
141
+ pause(): void {
142
+ if (this.player && this.isSpeaking && !this.isPaused) {
143
+ this.player.pause()
144
+ }
145
+ }
146
+
147
+ /**
148
+ * 恢复播放
149
+ */
150
+ resume(): void {
151
+ if (this.player && this.isPaused) {
152
+ this.player.resume()
153
+ }
154
+ }
155
+
156
+ /**
157
+ * 停止播放
158
+ */
159
+ async stop(): Promise<void> {
160
+ this.isSpeaking = false
161
+ this.isPaused = false
162
+
163
+ if (this.player) {
164
+ await this.player.stop()
165
+ }
166
+ }
167
+
168
+ /**
169
+ * 销毁 Speaker,释放资源
170
+ */
171
+ async destroy(): Promise<void> {
172
+ this.isSpeaking = false
173
+ this.isPaused = false
174
+
175
+ if (this.player) {
176
+ await this.player.stop()
177
+ this.player = undefined
178
+ }
179
+
180
+ this.currentProvider = undefined
181
+ this.currentText = undefined
182
+ }
183
+
184
+ /**
185
+ * 列出可用音色
186
+ */
187
+ async listVoices(providerName?: string): Promise<Voice[]> {
188
+ const name = providerName || this.options.defaultProvider || 'minimax'
189
+ const provider = getProvider(name)
190
+ return provider.listVoices()
191
+ }
192
+
193
+ /**
194
+ * 获取 Provider 能力
195
+ */
196
+ getCapabilities(providerName?: string) {
197
+ const name = providerName || this.options.defaultProvider || 'minimax'
198
+ const provider = getProvider(name)
199
+ return provider.getCapabilities()
200
+ }
201
+
202
+ /**
203
+ * 获取所有已注册的 Provider
204
+ */
205
+ getProviders(): string[] {
206
+ return listProviders()
207
+ }
208
+
209
+ /**
210
+ * 是否正在播放
211
+ */
212
+ isPlaying(): boolean {
213
+ return this.isSpeaking && !this.isPaused
214
+ }
215
+
216
+ /**
217
+ * 是否暂停
218
+ */
219
+ isPausedState(): boolean {
220
+ return this.isPaused
221
+ }
222
+ }
223
+
224
+ // ============================================================================
225
+ // 便捷函数 - 默认 Speaker 实例
226
+ // ============================================================================
227
+
228
+ let defaultSpeaker: Speaker | undefined
229
+
230
+ /**
231
+ * 获取默认 Speaker 实例(单例)
232
+ */
233
+ export function getDefaultSpeaker(): Speaker {
234
+ if (!defaultSpeaker) {
235
+ defaultSpeaker = new Speaker()
236
+ }
237
+ return defaultSpeaker
238
+ }
239
+
240
+ /**
241
+ * 说话(便捷函数)
242
+ */
243
+ export async function speak(
244
+ text: string,
245
+ options?: SpeakOptions & { provider?: string }
246
+ ): Promise<void> {
247
+ const speaker = getDefaultSpeaker()
248
+ return speaker.speak(text, options)
249
+ }
250
+
251
+ /**
252
+ * 停止(便捷函数)
253
+ */
254
+ export async function stop(): Promise<void> {
255
+ const speaker = getDefaultSpeaker()
256
+ return speaker.stop()
257
+ }
258
+
259
+ /**
260
+ * 暂停(便捷函数)
261
+ */
262
+ export function pause(): void {
263
+ const speaker = getDefaultSpeaker()
264
+ speaker.pause()
265
+ }
266
+
267
+ /**
268
+ * 恢复(便捷函数)
269
+ */
270
+ export function resume(): void {
271
+ const speaker = getDefaultSpeaker()
272
+ speaker.resume()
273
+ }
274
+
275
+ /**
276
+ * 列出音色(便捷函数)
277
+ */
278
+ export async function listVoices(providerName?: string): Promise<Voice[]> {
279
+ const speaker = getDefaultSpeaker()
280
+ return speaker.listVoices(providerName)
281
+ }
282
+
283
+ export default Speaker
@@ -0,0 +1,326 @@
1
+ /**
2
+ * StreamPlayer - 真正的边收边播流式音频播放器
3
+ * 接收音频 chunk,同时写入临时文件并立即启动播放器播放
4
+ */
5
+
6
+ import { EventEmitter } from 'events'
7
+ import { spawn, ChildProcess } from 'child_process'
8
+ import fs from 'fs'
9
+ import { createWriteStream, WriteStream } from 'fs'
10
+ import { tmpdir } from 'os'
11
+ import { join } from 'path'
12
+
13
+ /**
14
+ * StreamPlayer Events - 流式播放器事件回调接口
15
+ */
16
+ export interface StreamPlayerEvents {
17
+ onProgress?: (bytesWritten: number) => void
18
+ onStart?: () => void
19
+ onEnd?: () => void
20
+ onError?: (error: Error) => void
21
+ onPause?: () => void
22
+ onResume?: () => void
23
+ onStop?: () => void
24
+ }
25
+
26
+ /**
27
+ * StreamPlayer Options - 流式播放器配置选项
28
+ */
29
+ export interface StreamPlayerOptions {
30
+ format?: 'mp3' | 'wav' | 'flac'
31
+ events?: StreamPlayerEvents
32
+ }
33
+
34
+ /**
35
+ * StreamPlayer - 边收边播的流式音频播放器
36
+ *
37
+ * 特性:
38
+ * - 写入临时文件的同时立即启动播放器
39
+ * - 支持 pause/resume/stop 控制
40
+ * - 跨平台支持:macOS (afplay), Linux (aplay), Windows (PowerShell)
41
+ */
42
+ export class StreamPlayer extends EventEmitter {
43
+ private tempFile: string = ''
44
+ private writeStream?: WriteStream
45
+ private playerProcess?: ChildProcess
46
+ private _bytesWritten = 0
47
+ private _started = false
48
+ private _paused = false
49
+ private _stopped = false
50
+ private format: 'mp3' | 'wav' | 'flac' = 'mp3'
51
+ private events?: StreamPlayerEvents
52
+
53
+ constructor(options: StreamPlayerOptions = {}) {
54
+ super()
55
+ this.format = options.format || 'mp3'
56
+ this.events = options.events
57
+ }
58
+
59
+ /**
60
+ * 开始播放
61
+ * 创建临时文件,创建写入流,启动播放器进程
62
+ */
63
+ start(): void {
64
+ if (this._started) {
65
+ return
66
+ }
67
+
68
+ // 创建临时文件
69
+ this.tempFile = join(tmpdir(), `ocosay-stream-${Date.now()}.${this.format}`)
70
+
71
+ // 创建写入流
72
+ this.writeStream = createWriteStream(this.tempFile, { highWaterMark: 64 * 1024 })
73
+
74
+ this.writeStream.on('error', (error: Error) => {
75
+ this.handleError(error)
76
+ })
77
+
78
+ this.writeStream.on('finish', () => {
79
+ // 写入完成,但播放器可能还在播放
80
+ })
81
+
82
+ // 启动播放器进程
83
+ this.startPlayer()
84
+
85
+ this._started = true
86
+ this._stopped = false
87
+
88
+ this.events?.onStart?.()
89
+ this.emit('start')
90
+ }
91
+
92
+ /**
93
+ * 启动播放器进程
94
+ */
95
+ private startPlayer(): void {
96
+ const platform = process.platform
97
+ let command: string
98
+ let args: string[]
99
+
100
+ if (platform === 'darwin') {
101
+ // macOS
102
+ command = 'afplay'
103
+ args = [this.tempFile]
104
+ } else if (platform === 'linux') {
105
+ // Linux
106
+ command = 'aplay'
107
+ args = [this.tempFile]
108
+ } else {
109
+ // Windows - PlaySync is synchronous and blocks the event loop
110
+ // Return error to indicate Windows is not supported for streaming
111
+ this.handleError(new Error('Windows platform is not supported for stream playback. PlaySync() blocks the Node.js event loop.'))
112
+ return
113
+ }
114
+
115
+ this.playerProcess = spawn(command, args, {
116
+ stdio: 'ignore',
117
+ detached: false
118
+ })
119
+
120
+ this.playerProcess.on('exit', (code: number | null, signal: string | null) => {
121
+ // 如果是正常结束或被信号终止,不当作错误
122
+ if (this._stopped) {
123
+ return
124
+ }
125
+
126
+ if (signal === 'SIGTERM' || signal === 'SIGINT') {
127
+ // 被主动停止
128
+ return
129
+ }
130
+
131
+ if (code === 0 || code === null) {
132
+ // 正常播放结束
133
+ this.events?.onEnd?.()
134
+ this.emit('end')
135
+ } else {
136
+ this.handleError(new Error(`Player exited with code ${code}`))
137
+ }
138
+ })
139
+
140
+ this.playerProcess.on('error', (error: Error) => {
141
+ this.handleError(error)
142
+ })
143
+ }
144
+
145
+ /**
146
+ * 写入音频数据块(边收边写)
147
+ * 如果尚未 start(),会自动调用
148
+ */
149
+ write(chunk: Buffer): void {
150
+ // 如果已停止,直接忽略
151
+ if (this._stopped) {
152
+ return
153
+ }
154
+
155
+ // 如果未启动,自动启动
156
+ if (!this._started) {
157
+ this.start()
158
+ }
159
+
160
+ // 写入数据到文件
161
+ if (this.writeStream) {
162
+ const canContinue = this.writeStream.write(chunk)
163
+
164
+ if (!canContinue) {
165
+ // 写入缓冲区满了,等待 drain 事件
166
+ this.writeStream.once('drain', () => {
167
+ // 可以继续写入
168
+ })
169
+ }
170
+
171
+ this._bytesWritten += chunk.length
172
+
173
+ this.events?.onProgress?.(this._bytesWritten)
174
+ this.emit('progress', this._bytesWritten)
175
+ }
176
+ }
177
+
178
+ /**
179
+ * 结束写入
180
+ * 关闭写入流,但不杀播放器进程,让它播完
181
+ */
182
+ end(): void {
183
+ if (this.writeStream) {
184
+ this.writeStream.end()
185
+ this.writeStream = undefined
186
+ }
187
+ }
188
+
189
+ /**
190
+ * 停止播放
191
+ * 杀死播放器进程,删除临时文件
192
+ */
193
+ stop(): void {
194
+ this._stopped = true
195
+ this._started = false
196
+ this._paused = false
197
+
198
+ // 杀死播放器进程
199
+ if (this.playerProcess) {
200
+ try {
201
+ this.playerProcess.kill('SIGTERM')
202
+ } catch (e) {
203
+ // 忽略错误
204
+ }
205
+ this.playerProcess = undefined
206
+ }
207
+
208
+ // 关闭写入流
209
+ if (this.writeStream) {
210
+ try {
211
+ this.writeStream.destroy()
212
+ } catch (e) {
213
+ // 忽略错误
214
+ }
215
+ this.writeStream = undefined
216
+ }
217
+
218
+ // 删除临时文件
219
+ this.deleteTempFile()
220
+
221
+ this.events?.onStop?.()
222
+ this.emit('stop')
223
+ }
224
+
225
+ /**
226
+ * 暂停播放
227
+ * 使用 SIGSTOP 暂停播放器进程
228
+ */
229
+ pause(): void {
230
+ if (!this._started || this._paused || this._stopped) {
231
+ return
232
+ }
233
+
234
+ if (this.playerProcess) {
235
+ try {
236
+ this.playerProcess.kill('SIGSTOP')
237
+ this._paused = true
238
+ this.events?.onPause?.()
239
+ this.emit('pause')
240
+ } catch (e) {
241
+ // 如果 kill 失败,忽略
242
+ }
243
+ }
244
+ }
245
+
246
+ /**
247
+ * 恢复播放
248
+ */
249
+ resume(): void {
250
+ if (!this._paused || this._stopped) {
251
+ return
252
+ }
253
+
254
+ if (this.playerProcess) {
255
+ try {
256
+ this.playerProcess.kill('SIGCONT')
257
+ this._paused = false
258
+ this.events?.onResume?.()
259
+ this.emit('resume')
260
+ } catch (e) {
261
+ // 如果 kill 失败,忽略
262
+ }
263
+ }
264
+ }
265
+
266
+ /**
267
+ * 是否已启动
268
+ */
269
+ isStarted(): boolean {
270
+ return this._started
271
+ }
272
+
273
+ /**
274
+ * 是否暂停
275
+ */
276
+ isPaused(): boolean {
277
+ return this._paused
278
+ }
279
+
280
+ /**
281
+ * 是否已停止
282
+ */
283
+ isStopped(): boolean {
284
+ return this._stopped
285
+ }
286
+
287
+ /**
288
+ * 获取已写入的字节数
289
+ */
290
+ getBytesWritten(): number {
291
+ return this._bytesWritten
292
+ }
293
+
294
+ /**
295
+ * 获取临时文件路径
296
+ */
297
+ getTempFile(): string {
298
+ return this.tempFile
299
+ }
300
+
301
+ /**
302
+ * 处理错误
303
+ */
304
+ private handleError(error: Error): void {
305
+ this.events?.onError?.(error)
306
+ this.emit('error', error)
307
+ }
308
+
309
+ /**
310
+ * 删除临时文件
311
+ */
312
+ private deleteTempFile(): void {
313
+ if (this.tempFile) {
314
+ try {
315
+ if (fs.existsSync(this.tempFile)) {
316
+ fs.unlinkSync(this.tempFile)
317
+ }
318
+ } catch (e) {
319
+ // 忽略删除错误
320
+ }
321
+ this.tempFile = ''
322
+ }
323
+ }
324
+ }
325
+
326
+ export default StreamPlayer