@wq-hook/volcano-react 1.0.3 → 1.0.5
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/index.d.mts +10 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +126 -17
- package/dist/index.mjs +126 -17
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -420,6 +420,7 @@ interface PlaybackSessionConfig {
|
|
|
420
420
|
onPlayResume?: () => void;
|
|
421
421
|
onPlayEnd?: () => void;
|
|
422
422
|
onError?: (error: Error) => void;
|
|
423
|
+
onSessionEnd?: (id: string) => void;
|
|
423
424
|
}
|
|
424
425
|
/**
|
|
425
426
|
* 播放会话控制器
|
|
@@ -447,6 +448,9 @@ declare class PlaybackSession {
|
|
|
447
448
|
private resolveAllSegmentsSent;
|
|
448
449
|
private animId;
|
|
449
450
|
private lastVisUpdate;
|
|
451
|
+
private pausedTime;
|
|
452
|
+
private cachedAudioData;
|
|
453
|
+
private isStopping;
|
|
450
454
|
constructor(id: string, config: PlaybackSessionConfig);
|
|
451
455
|
/**
|
|
452
456
|
* 初始化 AudioContext(用于可视化)
|
|
@@ -471,7 +475,7 @@ declare class PlaybackSession {
|
|
|
471
475
|
play(text: string): Promise<void>;
|
|
472
476
|
private processQueue;
|
|
473
477
|
pause(): void;
|
|
474
|
-
resume(): void
|
|
478
|
+
resume(): Promise<void>;
|
|
475
479
|
stop(): void;
|
|
476
480
|
seek(percentage: number): void;
|
|
477
481
|
private updateState;
|
|
@@ -481,6 +485,11 @@ declare class PlaybackSession {
|
|
|
481
485
|
private getTimeDomainData;
|
|
482
486
|
private startVisualizationLoop;
|
|
483
487
|
private stopVisualizationLoop;
|
|
488
|
+
/**
|
|
489
|
+
* 释放 Blob URL 资源
|
|
490
|
+
* 在暂停、停止、播放完毕时调用,避免 Blob URL 长期占用内存和过期问题
|
|
491
|
+
*/
|
|
492
|
+
private releaseBlobUrl;
|
|
484
493
|
}
|
|
485
494
|
/**
|
|
486
495
|
* 流式播放管理器(单例)
|
package/dist/index.d.ts
CHANGED
|
@@ -420,6 +420,7 @@ interface PlaybackSessionConfig {
|
|
|
420
420
|
onPlayResume?: () => void;
|
|
421
421
|
onPlayEnd?: () => void;
|
|
422
422
|
onError?: (error: Error) => void;
|
|
423
|
+
onSessionEnd?: (id: string) => void;
|
|
423
424
|
}
|
|
424
425
|
/**
|
|
425
426
|
* 播放会话控制器
|
|
@@ -447,6 +448,9 @@ declare class PlaybackSession {
|
|
|
447
448
|
private resolveAllSegmentsSent;
|
|
448
449
|
private animId;
|
|
449
450
|
private lastVisUpdate;
|
|
451
|
+
private pausedTime;
|
|
452
|
+
private cachedAudioData;
|
|
453
|
+
private isStopping;
|
|
450
454
|
constructor(id: string, config: PlaybackSessionConfig);
|
|
451
455
|
/**
|
|
452
456
|
* 初始化 AudioContext(用于可视化)
|
|
@@ -471,7 +475,7 @@ declare class PlaybackSession {
|
|
|
471
475
|
play(text: string): Promise<void>;
|
|
472
476
|
private processQueue;
|
|
473
477
|
pause(): void;
|
|
474
|
-
resume(): void
|
|
478
|
+
resume(): Promise<void>;
|
|
475
479
|
stop(): void;
|
|
476
480
|
seek(percentage: number): void;
|
|
477
481
|
private updateState;
|
|
@@ -481,6 +485,11 @@ declare class PlaybackSession {
|
|
|
481
485
|
private getTimeDomainData;
|
|
482
486
|
private startVisualizationLoop;
|
|
483
487
|
private stopVisualizationLoop;
|
|
488
|
+
/**
|
|
489
|
+
* 释放 Blob URL 资源
|
|
490
|
+
* 在暂停、停止、播放完毕时调用,避免 Blob URL 长期占用内存和过期问题
|
|
491
|
+
*/
|
|
492
|
+
private releaseBlobUrl;
|
|
484
493
|
}
|
|
485
494
|
/**
|
|
486
495
|
* 流式播放管理器(单例)
|
package/dist/index.js
CHANGED
|
@@ -770,6 +770,7 @@ function buildFullUrl2(url, params) {
|
|
|
770
770
|
return `${url}?${arr.join("&")}`;
|
|
771
771
|
}
|
|
772
772
|
var PlaybackSession = class {
|
|
773
|
+
// 标记是否正在停止,用于区分 stop() 和 pause()
|
|
773
774
|
constructor(id, config) {
|
|
774
775
|
this.listeners = /* @__PURE__ */ new Set();
|
|
775
776
|
this.audioContext = null;
|
|
@@ -790,6 +791,12 @@ var PlaybackSession = class {
|
|
|
790
791
|
this.resolveAllSegmentsSent = null;
|
|
791
792
|
this.animId = null;
|
|
792
793
|
this.lastVisUpdate = 0;
|
|
794
|
+
// Blob URL 管理状态
|
|
795
|
+
this.pausedTime = 0;
|
|
796
|
+
// 记录暂停时的播放位置
|
|
797
|
+
this.cachedAudioData = null;
|
|
798
|
+
// 缓存音频数据,用于恢复时重新创建 Blob URL
|
|
799
|
+
this.isStopping = false;
|
|
793
800
|
this.id = id;
|
|
794
801
|
this.config = config;
|
|
795
802
|
this.state = {
|
|
@@ -842,6 +849,7 @@ var PlaybackSession = class {
|
|
|
842
849
|
this.startVisualizationLoop();
|
|
843
850
|
};
|
|
844
851
|
this.audio.onpause = () => {
|
|
852
|
+
if (this.isStopping) return;
|
|
845
853
|
this.updateState({ isPaused: true, isPlaying: false });
|
|
846
854
|
this.config.onPlayPause?.();
|
|
847
855
|
};
|
|
@@ -850,14 +858,45 @@ var PlaybackSession = class {
|
|
|
850
858
|
isPlaying: false,
|
|
851
859
|
isPaused: false,
|
|
852
860
|
isSynthesizing: false,
|
|
853
|
-
progress:
|
|
861
|
+
progress: 0,
|
|
862
|
+
visualizationData: {
|
|
863
|
+
frequencyData: new Uint8Array(0),
|
|
864
|
+
timeDomainData: new Uint8Array(0)
|
|
865
|
+
}
|
|
854
866
|
});
|
|
855
867
|
this.config.onPlayEnd?.();
|
|
868
|
+
this.releaseBlobUrl();
|
|
869
|
+
this.pausedTime = 0;
|
|
856
870
|
this.stopVisualizationLoop();
|
|
871
|
+
this.config.onSessionEnd?.(this.id);
|
|
857
872
|
};
|
|
858
|
-
this.audio.onerror = (e) => {
|
|
873
|
+
this.audio.onerror = async (e) => {
|
|
859
874
|
const msg = this.audio.error?.message || "Audio playback error";
|
|
875
|
+
if (msg.includes("Empty src") || msg.includes("empty src")) {
|
|
876
|
+
console.log("[PlaybackSession] Ignoring empty src error during transition");
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
860
879
|
console.error("[PlaybackSession] Audio error:", msg);
|
|
880
|
+
const isBlobUrlExpired = msg.includes("ERR_FILE_NOT_FOUND") || msg.includes("PIPELINE_ERROR_READ") || msg.includes("MEDIA_ELEMENT_ERROR") || this.audio.error?.code === MediaError.MEDIA_ERR_NETWORK || this.audio.error?.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED;
|
|
881
|
+
if (isBlobUrlExpired && this.cachedAudioData) {
|
|
882
|
+
console.warn(
|
|
883
|
+
"[PlaybackSession] Blob URL expired, attempting to recreate from cache"
|
|
884
|
+
);
|
|
885
|
+
this.releaseBlobUrl();
|
|
886
|
+
const blob = new Blob(this.cachedAudioData, { type: "audio/mpeg" });
|
|
887
|
+
this.audioUrl = URL.createObjectURL(blob);
|
|
888
|
+
this.audio.src = this.audioUrl;
|
|
889
|
+
const resumeTime = this.pausedTime || 0;
|
|
890
|
+
try {
|
|
891
|
+
await this.audio.play();
|
|
892
|
+
if (resumeTime > 0) {
|
|
893
|
+
this.audio.currentTime = resumeTime;
|
|
894
|
+
}
|
|
895
|
+
return;
|
|
896
|
+
} catch (playErr) {
|
|
897
|
+
console.error("[PlaybackSession] Failed to replay from cache:", playErr);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
861
900
|
this.updateState({ error: msg });
|
|
862
901
|
this.config.onError?.(new Error(msg));
|
|
863
902
|
};
|
|
@@ -977,6 +1016,9 @@ var PlaybackSession = class {
|
|
|
977
1016
|
isSynthesizing: false,
|
|
978
1017
|
isSessionStarted: false
|
|
979
1018
|
});
|
|
1019
|
+
if (this.sessionAudioBuffers.length > 0) {
|
|
1020
|
+
this.cachedAudioData = [...this.sessionAudioBuffers];
|
|
1021
|
+
}
|
|
980
1022
|
if (this.sessionAudioBuffers.length > 0 && this.streamText) {
|
|
981
1023
|
const speed = audioParams?.speech_rate || 0;
|
|
982
1024
|
const cacheKey = TTSCache.generateKey(
|
|
@@ -1081,11 +1123,12 @@ var PlaybackSession = class {
|
|
|
1081
1123
|
const cacheKey = TTSCache.generateKey(formattedText, voice, speed);
|
|
1082
1124
|
const cachedData = await TTSCache.get(cacheKey);
|
|
1083
1125
|
if (cachedData && cachedData.length > 0) {
|
|
1126
|
+
this.cachedAudioData = cachedData;
|
|
1127
|
+
this.releaseBlobUrl();
|
|
1084
1128
|
const blob = new Blob(cachedData, { type: "audio/mpeg" });
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
this.
|
|
1088
|
-
this.audio.src = url;
|
|
1129
|
+
this.audioUrl = URL.createObjectURL(blob);
|
|
1130
|
+
this.audio.src = this.audioUrl;
|
|
1131
|
+
this.pausedTime = 0;
|
|
1089
1132
|
this.updateState({ isSynthesizing: false });
|
|
1090
1133
|
if (this.config.autoPlay !== false) {
|
|
1091
1134
|
try {
|
|
@@ -1138,24 +1181,52 @@ var PlaybackSession = class {
|
|
|
1138
1181
|
setTimeout(() => this.processQueue(), 0);
|
|
1139
1182
|
}
|
|
1140
1183
|
pause() {
|
|
1184
|
+
if (this.isStopping) return;
|
|
1185
|
+
this.pausedTime = this.audio.currentTime;
|
|
1141
1186
|
this.audio.pause();
|
|
1187
|
+
this.releaseBlobUrl();
|
|
1142
1188
|
this.updateState({ isPaused: true, isPlaying: false });
|
|
1143
1189
|
}
|
|
1144
|
-
resume() {
|
|
1145
|
-
this.
|
|
1190
|
+
async resume() {
|
|
1191
|
+
if (!this.audioUrl && this.cachedAudioData) {
|
|
1192
|
+
const blob = new Blob(this.cachedAudioData, { type: "audio/mpeg" });
|
|
1193
|
+
this.audioUrl = URL.createObjectURL(blob);
|
|
1194
|
+
this.audio.src = this.audioUrl;
|
|
1195
|
+
await new Promise((resolve, reject) => {
|
|
1196
|
+
const onLoaded = () => {
|
|
1197
|
+
resolve();
|
|
1198
|
+
this.audio.removeEventListener("loadedmetadata", onLoaded);
|
|
1199
|
+
this.audio.removeEventListener("error", onError);
|
|
1200
|
+
};
|
|
1201
|
+
const onError = () => {
|
|
1202
|
+
reject(new Error("Failed to load audio"));
|
|
1203
|
+
this.audio.removeEventListener("loadedmetadata", onLoaded);
|
|
1204
|
+
this.audio.removeEventListener("error", onError);
|
|
1205
|
+
};
|
|
1206
|
+
this.audio.addEventListener("loadedmetadata", onLoaded);
|
|
1207
|
+
this.audio.addEventListener("error", onError);
|
|
1208
|
+
setTimeout(() => {
|
|
1209
|
+
this.audio.removeEventListener("loadedmetadata", onLoaded);
|
|
1210
|
+
this.audio.removeEventListener("error", onError);
|
|
1211
|
+
resolve();
|
|
1212
|
+
}, 3e3);
|
|
1213
|
+
});
|
|
1214
|
+
this.audio.currentTime = this.pausedTime;
|
|
1215
|
+
}
|
|
1216
|
+
await this.audio.play();
|
|
1146
1217
|
this.updateState({ isPaused: false, isPlaying: true });
|
|
1147
1218
|
}
|
|
1148
1219
|
stop() {
|
|
1220
|
+
this.isStopping = true;
|
|
1149
1221
|
if (this.client) {
|
|
1150
1222
|
this.client.close();
|
|
1151
1223
|
this.client = null;
|
|
1152
1224
|
}
|
|
1153
1225
|
this.audio.pause();
|
|
1154
1226
|
this.audio.currentTime = 0;
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
}
|
|
1227
|
+
this.releaseBlobUrl();
|
|
1228
|
+
this.cachedAudioData = null;
|
|
1229
|
+
this.pausedTime = 0;
|
|
1159
1230
|
this.stopVisualizationLoop();
|
|
1160
1231
|
this.audioContext?.close();
|
|
1161
1232
|
this.audioContext = null;
|
|
@@ -1165,8 +1236,14 @@ var PlaybackSession = class {
|
|
|
1165
1236
|
isSynthesizing: false,
|
|
1166
1237
|
progress: 0,
|
|
1167
1238
|
isConnected: false,
|
|
1168
|
-
isSessionStarted: false
|
|
1239
|
+
isSessionStarted: false,
|
|
1240
|
+
// 清除可视化数据
|
|
1241
|
+
visualizationData: {
|
|
1242
|
+
frequencyData: new Uint8Array(0),
|
|
1243
|
+
timeDomainData: new Uint8Array(0)
|
|
1244
|
+
}
|
|
1169
1245
|
});
|
|
1246
|
+
this.isStopping = false;
|
|
1170
1247
|
}
|
|
1171
1248
|
seek(percentage) {
|
|
1172
1249
|
let duration = this.audio.duration;
|
|
@@ -1230,6 +1307,18 @@ var PlaybackSession = class {
|
|
|
1230
1307
|
this.animId = null;
|
|
1231
1308
|
}
|
|
1232
1309
|
}
|
|
1310
|
+
/**
|
|
1311
|
+
* 释放 Blob URL 资源
|
|
1312
|
+
* 在暂停、停止、播放完毕时调用,避免 Blob URL 长期占用内存和过期问题
|
|
1313
|
+
*/
|
|
1314
|
+
releaseBlobUrl() {
|
|
1315
|
+
if (this.audioUrl) {
|
|
1316
|
+
URL.revokeObjectURL(this.audioUrl);
|
|
1317
|
+
this.audioUrl = null;
|
|
1318
|
+
}
|
|
1319
|
+
this.audio.src = "";
|
|
1320
|
+
this.audio.load();
|
|
1321
|
+
}
|
|
1233
1322
|
};
|
|
1234
1323
|
var StreamPlaybackManagerImpl = class {
|
|
1235
1324
|
constructor() {
|
|
@@ -1241,9 +1330,28 @@ var StreamPlaybackManagerImpl = class {
|
|
|
1241
1330
|
*/
|
|
1242
1331
|
createSession(id, config) {
|
|
1243
1332
|
if (this.activeStreamId && this.activeStreamId !== id) {
|
|
1244
|
-
this.
|
|
1333
|
+
const activeSession = this.sessions.get(this.activeStreamId);
|
|
1334
|
+
if (activeSession) {
|
|
1335
|
+
const isPlaying = activeSession.state.isPlaying;
|
|
1336
|
+
const isPaused = activeSession.state.isPaused;
|
|
1337
|
+
console.log(`[StreamPlaybackManager] Checking active session ${this.activeStreamId}: isPlaying=${isPlaying}, isPaused=${isPaused}`);
|
|
1338
|
+
if (isPlaying || isPaused) {
|
|
1339
|
+
console.log(`[StreamPlaybackManager] Pausing active session ${this.activeStreamId}`);
|
|
1340
|
+
this.pause(this.activeStreamId);
|
|
1341
|
+
} else {
|
|
1342
|
+
console.log(`[StreamPlaybackManager] Active session ${this.activeStreamId} is not playing/paused, skipping pause`);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1245
1345
|
}
|
|
1246
|
-
const session = new PlaybackSession(id,
|
|
1346
|
+
const session = new PlaybackSession(id, {
|
|
1347
|
+
...config,
|
|
1348
|
+
onSessionEnd: (sessionId) => {
|
|
1349
|
+
if (this.activeStreamId === sessionId) {
|
|
1350
|
+
this.activeStreamId = null;
|
|
1351
|
+
}
|
|
1352
|
+
config.onSessionEnd?.(sessionId);
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1247
1355
|
this.sessions.set(id, session);
|
|
1248
1356
|
this.activeStreamId = id;
|
|
1249
1357
|
return session;
|
|
@@ -1781,9 +1889,10 @@ var AudioWaveVisualizer = ({
|
|
|
1781
1889
|
ctx.strokeStyle = fillStyle;
|
|
1782
1890
|
if (style === "bar" && frequencyData) {
|
|
1783
1891
|
const barWidth = w / bars;
|
|
1784
|
-
const step = Math.floor(frequencyData.length / bars);
|
|
1785
1892
|
for (let i = 0; i < bars; i++) {
|
|
1786
|
-
const
|
|
1893
|
+
const halfLength = frequencyData.length / 2;
|
|
1894
|
+
const index = Math.floor(i / bars * halfLength);
|
|
1895
|
+
const value = frequencyData[index] || 0;
|
|
1787
1896
|
const percent = value / 255;
|
|
1788
1897
|
const barHeight = h * percent;
|
|
1789
1898
|
ctx.fillRect(i * barWidth, h - barHeight, barWidth - 2, barHeight);
|
package/dist/index.mjs
CHANGED
|
@@ -726,6 +726,7 @@ function buildFullUrl2(url, params) {
|
|
|
726
726
|
return `${url}?${arr.join("&")}`;
|
|
727
727
|
}
|
|
728
728
|
var PlaybackSession = class {
|
|
729
|
+
// 标记是否正在停止,用于区分 stop() 和 pause()
|
|
729
730
|
constructor(id, config) {
|
|
730
731
|
this.listeners = /* @__PURE__ */ new Set();
|
|
731
732
|
this.audioContext = null;
|
|
@@ -746,6 +747,12 @@ var PlaybackSession = class {
|
|
|
746
747
|
this.resolveAllSegmentsSent = null;
|
|
747
748
|
this.animId = null;
|
|
748
749
|
this.lastVisUpdate = 0;
|
|
750
|
+
// Blob URL 管理状态
|
|
751
|
+
this.pausedTime = 0;
|
|
752
|
+
// 记录暂停时的播放位置
|
|
753
|
+
this.cachedAudioData = null;
|
|
754
|
+
// 缓存音频数据,用于恢复时重新创建 Blob URL
|
|
755
|
+
this.isStopping = false;
|
|
749
756
|
this.id = id;
|
|
750
757
|
this.config = config;
|
|
751
758
|
this.state = {
|
|
@@ -798,6 +805,7 @@ var PlaybackSession = class {
|
|
|
798
805
|
this.startVisualizationLoop();
|
|
799
806
|
};
|
|
800
807
|
this.audio.onpause = () => {
|
|
808
|
+
if (this.isStopping) return;
|
|
801
809
|
this.updateState({ isPaused: true, isPlaying: false });
|
|
802
810
|
this.config.onPlayPause?.();
|
|
803
811
|
};
|
|
@@ -806,14 +814,45 @@ var PlaybackSession = class {
|
|
|
806
814
|
isPlaying: false,
|
|
807
815
|
isPaused: false,
|
|
808
816
|
isSynthesizing: false,
|
|
809
|
-
progress:
|
|
817
|
+
progress: 0,
|
|
818
|
+
visualizationData: {
|
|
819
|
+
frequencyData: new Uint8Array(0),
|
|
820
|
+
timeDomainData: new Uint8Array(0)
|
|
821
|
+
}
|
|
810
822
|
});
|
|
811
823
|
this.config.onPlayEnd?.();
|
|
824
|
+
this.releaseBlobUrl();
|
|
825
|
+
this.pausedTime = 0;
|
|
812
826
|
this.stopVisualizationLoop();
|
|
827
|
+
this.config.onSessionEnd?.(this.id);
|
|
813
828
|
};
|
|
814
|
-
this.audio.onerror = (e) => {
|
|
829
|
+
this.audio.onerror = async (e) => {
|
|
815
830
|
const msg = this.audio.error?.message || "Audio playback error";
|
|
831
|
+
if (msg.includes("Empty src") || msg.includes("empty src")) {
|
|
832
|
+
console.log("[PlaybackSession] Ignoring empty src error during transition");
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
816
835
|
console.error("[PlaybackSession] Audio error:", msg);
|
|
836
|
+
const isBlobUrlExpired = msg.includes("ERR_FILE_NOT_FOUND") || msg.includes("PIPELINE_ERROR_READ") || msg.includes("MEDIA_ELEMENT_ERROR") || this.audio.error?.code === MediaError.MEDIA_ERR_NETWORK || this.audio.error?.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED;
|
|
837
|
+
if (isBlobUrlExpired && this.cachedAudioData) {
|
|
838
|
+
console.warn(
|
|
839
|
+
"[PlaybackSession] Blob URL expired, attempting to recreate from cache"
|
|
840
|
+
);
|
|
841
|
+
this.releaseBlobUrl();
|
|
842
|
+
const blob = new Blob(this.cachedAudioData, { type: "audio/mpeg" });
|
|
843
|
+
this.audioUrl = URL.createObjectURL(blob);
|
|
844
|
+
this.audio.src = this.audioUrl;
|
|
845
|
+
const resumeTime = this.pausedTime || 0;
|
|
846
|
+
try {
|
|
847
|
+
await this.audio.play();
|
|
848
|
+
if (resumeTime > 0) {
|
|
849
|
+
this.audio.currentTime = resumeTime;
|
|
850
|
+
}
|
|
851
|
+
return;
|
|
852
|
+
} catch (playErr) {
|
|
853
|
+
console.error("[PlaybackSession] Failed to replay from cache:", playErr);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
817
856
|
this.updateState({ error: msg });
|
|
818
857
|
this.config.onError?.(new Error(msg));
|
|
819
858
|
};
|
|
@@ -933,6 +972,9 @@ var PlaybackSession = class {
|
|
|
933
972
|
isSynthesizing: false,
|
|
934
973
|
isSessionStarted: false
|
|
935
974
|
});
|
|
975
|
+
if (this.sessionAudioBuffers.length > 0) {
|
|
976
|
+
this.cachedAudioData = [...this.sessionAudioBuffers];
|
|
977
|
+
}
|
|
936
978
|
if (this.sessionAudioBuffers.length > 0 && this.streamText) {
|
|
937
979
|
const speed = audioParams?.speech_rate || 0;
|
|
938
980
|
const cacheKey = TTSCache.generateKey(
|
|
@@ -1037,11 +1079,12 @@ var PlaybackSession = class {
|
|
|
1037
1079
|
const cacheKey = TTSCache.generateKey(formattedText, voice, speed);
|
|
1038
1080
|
const cachedData = await TTSCache.get(cacheKey);
|
|
1039
1081
|
if (cachedData && cachedData.length > 0) {
|
|
1082
|
+
this.cachedAudioData = cachedData;
|
|
1083
|
+
this.releaseBlobUrl();
|
|
1040
1084
|
const blob = new Blob(cachedData, { type: "audio/mpeg" });
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
this.
|
|
1044
|
-
this.audio.src = url;
|
|
1085
|
+
this.audioUrl = URL.createObjectURL(blob);
|
|
1086
|
+
this.audio.src = this.audioUrl;
|
|
1087
|
+
this.pausedTime = 0;
|
|
1045
1088
|
this.updateState({ isSynthesizing: false });
|
|
1046
1089
|
if (this.config.autoPlay !== false) {
|
|
1047
1090
|
try {
|
|
@@ -1094,24 +1137,52 @@ var PlaybackSession = class {
|
|
|
1094
1137
|
setTimeout(() => this.processQueue(), 0);
|
|
1095
1138
|
}
|
|
1096
1139
|
pause() {
|
|
1140
|
+
if (this.isStopping) return;
|
|
1141
|
+
this.pausedTime = this.audio.currentTime;
|
|
1097
1142
|
this.audio.pause();
|
|
1143
|
+
this.releaseBlobUrl();
|
|
1098
1144
|
this.updateState({ isPaused: true, isPlaying: false });
|
|
1099
1145
|
}
|
|
1100
|
-
resume() {
|
|
1101
|
-
this.
|
|
1146
|
+
async resume() {
|
|
1147
|
+
if (!this.audioUrl && this.cachedAudioData) {
|
|
1148
|
+
const blob = new Blob(this.cachedAudioData, { type: "audio/mpeg" });
|
|
1149
|
+
this.audioUrl = URL.createObjectURL(blob);
|
|
1150
|
+
this.audio.src = this.audioUrl;
|
|
1151
|
+
await new Promise((resolve, reject) => {
|
|
1152
|
+
const onLoaded = () => {
|
|
1153
|
+
resolve();
|
|
1154
|
+
this.audio.removeEventListener("loadedmetadata", onLoaded);
|
|
1155
|
+
this.audio.removeEventListener("error", onError);
|
|
1156
|
+
};
|
|
1157
|
+
const onError = () => {
|
|
1158
|
+
reject(new Error("Failed to load audio"));
|
|
1159
|
+
this.audio.removeEventListener("loadedmetadata", onLoaded);
|
|
1160
|
+
this.audio.removeEventListener("error", onError);
|
|
1161
|
+
};
|
|
1162
|
+
this.audio.addEventListener("loadedmetadata", onLoaded);
|
|
1163
|
+
this.audio.addEventListener("error", onError);
|
|
1164
|
+
setTimeout(() => {
|
|
1165
|
+
this.audio.removeEventListener("loadedmetadata", onLoaded);
|
|
1166
|
+
this.audio.removeEventListener("error", onError);
|
|
1167
|
+
resolve();
|
|
1168
|
+
}, 3e3);
|
|
1169
|
+
});
|
|
1170
|
+
this.audio.currentTime = this.pausedTime;
|
|
1171
|
+
}
|
|
1172
|
+
await this.audio.play();
|
|
1102
1173
|
this.updateState({ isPaused: false, isPlaying: true });
|
|
1103
1174
|
}
|
|
1104
1175
|
stop() {
|
|
1176
|
+
this.isStopping = true;
|
|
1105
1177
|
if (this.client) {
|
|
1106
1178
|
this.client.close();
|
|
1107
1179
|
this.client = null;
|
|
1108
1180
|
}
|
|
1109
1181
|
this.audio.pause();
|
|
1110
1182
|
this.audio.currentTime = 0;
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
}
|
|
1183
|
+
this.releaseBlobUrl();
|
|
1184
|
+
this.cachedAudioData = null;
|
|
1185
|
+
this.pausedTime = 0;
|
|
1115
1186
|
this.stopVisualizationLoop();
|
|
1116
1187
|
this.audioContext?.close();
|
|
1117
1188
|
this.audioContext = null;
|
|
@@ -1121,8 +1192,14 @@ var PlaybackSession = class {
|
|
|
1121
1192
|
isSynthesizing: false,
|
|
1122
1193
|
progress: 0,
|
|
1123
1194
|
isConnected: false,
|
|
1124
|
-
isSessionStarted: false
|
|
1195
|
+
isSessionStarted: false,
|
|
1196
|
+
// 清除可视化数据
|
|
1197
|
+
visualizationData: {
|
|
1198
|
+
frequencyData: new Uint8Array(0),
|
|
1199
|
+
timeDomainData: new Uint8Array(0)
|
|
1200
|
+
}
|
|
1125
1201
|
});
|
|
1202
|
+
this.isStopping = false;
|
|
1126
1203
|
}
|
|
1127
1204
|
seek(percentage) {
|
|
1128
1205
|
let duration = this.audio.duration;
|
|
@@ -1186,6 +1263,18 @@ var PlaybackSession = class {
|
|
|
1186
1263
|
this.animId = null;
|
|
1187
1264
|
}
|
|
1188
1265
|
}
|
|
1266
|
+
/**
|
|
1267
|
+
* 释放 Blob URL 资源
|
|
1268
|
+
* 在暂停、停止、播放完毕时调用,避免 Blob URL 长期占用内存和过期问题
|
|
1269
|
+
*/
|
|
1270
|
+
releaseBlobUrl() {
|
|
1271
|
+
if (this.audioUrl) {
|
|
1272
|
+
URL.revokeObjectURL(this.audioUrl);
|
|
1273
|
+
this.audioUrl = null;
|
|
1274
|
+
}
|
|
1275
|
+
this.audio.src = "";
|
|
1276
|
+
this.audio.load();
|
|
1277
|
+
}
|
|
1189
1278
|
};
|
|
1190
1279
|
var StreamPlaybackManagerImpl = class {
|
|
1191
1280
|
constructor() {
|
|
@@ -1197,9 +1286,28 @@ var StreamPlaybackManagerImpl = class {
|
|
|
1197
1286
|
*/
|
|
1198
1287
|
createSession(id, config) {
|
|
1199
1288
|
if (this.activeStreamId && this.activeStreamId !== id) {
|
|
1200
|
-
this.
|
|
1289
|
+
const activeSession = this.sessions.get(this.activeStreamId);
|
|
1290
|
+
if (activeSession) {
|
|
1291
|
+
const isPlaying = activeSession.state.isPlaying;
|
|
1292
|
+
const isPaused = activeSession.state.isPaused;
|
|
1293
|
+
console.log(`[StreamPlaybackManager] Checking active session ${this.activeStreamId}: isPlaying=${isPlaying}, isPaused=${isPaused}`);
|
|
1294
|
+
if (isPlaying || isPaused) {
|
|
1295
|
+
console.log(`[StreamPlaybackManager] Pausing active session ${this.activeStreamId}`);
|
|
1296
|
+
this.pause(this.activeStreamId);
|
|
1297
|
+
} else {
|
|
1298
|
+
console.log(`[StreamPlaybackManager] Active session ${this.activeStreamId} is not playing/paused, skipping pause`);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1201
1301
|
}
|
|
1202
|
-
const session = new PlaybackSession(id,
|
|
1302
|
+
const session = new PlaybackSession(id, {
|
|
1303
|
+
...config,
|
|
1304
|
+
onSessionEnd: (sessionId) => {
|
|
1305
|
+
if (this.activeStreamId === sessionId) {
|
|
1306
|
+
this.activeStreamId = null;
|
|
1307
|
+
}
|
|
1308
|
+
config.onSessionEnd?.(sessionId);
|
|
1309
|
+
}
|
|
1310
|
+
});
|
|
1203
1311
|
this.sessions.set(id, session);
|
|
1204
1312
|
this.activeStreamId = id;
|
|
1205
1313
|
return session;
|
|
@@ -1737,9 +1845,10 @@ var AudioWaveVisualizer = ({
|
|
|
1737
1845
|
ctx.strokeStyle = fillStyle;
|
|
1738
1846
|
if (style === "bar" && frequencyData) {
|
|
1739
1847
|
const barWidth = w / bars;
|
|
1740
|
-
const step = Math.floor(frequencyData.length / bars);
|
|
1741
1848
|
for (let i = 0; i < bars; i++) {
|
|
1742
|
-
const
|
|
1849
|
+
const halfLength = frequencyData.length / 2;
|
|
1850
|
+
const index = Math.floor(i / bars * halfLength);
|
|
1851
|
+
const value = frequencyData[index] || 0;
|
|
1743
1852
|
const percent = value / 255;
|
|
1744
1853
|
const barHeight = h * percent;
|
|
1745
1854
|
ctx.fillRect(i * barWidth, h - barHeight, barWidth - 2, barHeight);
|