@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 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: 100
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
- const url = URL.createObjectURL(blob);
1086
- if (this.audioUrl) URL.revokeObjectURL(this.audioUrl);
1087
- this.audioUrl = url;
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.audio.play();
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
- if (this.audioUrl) {
1156
- URL.revokeObjectURL(this.audioUrl);
1157
- this.audioUrl = null;
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.pause(this.activeStreamId);
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, config);
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 value = frequencyData[i * step] || 0;
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: 100
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
- const url = URL.createObjectURL(blob);
1042
- if (this.audioUrl) URL.revokeObjectURL(this.audioUrl);
1043
- this.audioUrl = url;
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.audio.play();
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
- if (this.audioUrl) {
1112
- URL.revokeObjectURL(this.audioUrl);
1113
- this.audioUrl = null;
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.pause(this.activeStreamId);
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, config);
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 value = frequencyData[i * step] || 0;
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wq-hook/volcano-react",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Volcano Engine ASR & TTS React Hooks",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",