@rimori/react-client 0.3.0-next.7 → 0.3.0-next.8

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.
@@ -26,6 +26,12 @@ export function AssistantChat({ avatarImageUrl, voiceId, onComplete, autoStartCo
26
26
  sender.handleNewText(autoStartConversation.assistantMessage, isLoading);
27
27
  }
28
28
  }, []);
29
+ // Cleanup: stop audio playback and clean up resources on unmount
30
+ useEffect(() => {
31
+ return () => {
32
+ sender.cleanup();
33
+ };
34
+ }, [sender]);
29
35
  useEffect(() => {
30
36
  var _a;
31
37
  let message = lastAssistantMessage;
@@ -38,6 +38,12 @@ export function Avatar({ avatarImageUrl, voiceId, agentTools, autoStartConversat
38
38
  append([{ role: 'user', content: autoStartConversation.userMessage, id: messages.length.toString() }]);
39
39
  }
40
40
  }, [autoStartConversation, voiceId]);
41
+ // Cleanup: stop audio playback and clean up resources on unmount
42
+ useEffect(() => {
43
+ return () => {
44
+ sender.cleanup();
45
+ };
46
+ }, [sender]);
41
47
  useEffect(() => {
42
48
  if ((lastMessage === null || lastMessage === void 0 ? void 0 : lastMessage.role) === 'assistant') {
43
49
  sender.handleNewText(lastMessage.content, isLoading);
@@ -11,6 +11,7 @@ export declare class MessageSender {
11
11
  private generateSpeech;
12
12
  play(): void;
13
13
  stop(): void;
14
+ cleanup(): void;
14
15
  private reset;
15
16
  setVolume(volume: number): void;
16
17
  setOnLoudnessChange(callback: (value: number) => void): void;
@@ -72,6 +72,9 @@ export class MessageSender {
72
72
  stop() {
73
73
  this.player.stopPlayback();
74
74
  }
75
+ cleanup() {
76
+ this.player.cleanup();
77
+ }
75
78
  reset() {
76
79
  this.stop();
77
80
  this.fetchedSentences.clear();
@@ -20,6 +20,7 @@ export declare class ChunkedAudioPlayer {
20
20
  addChunk(chunk: ArrayBuffer, position: number): Promise<void>;
21
21
  private playChunks;
22
22
  stopPlayback(): void;
23
+ cleanup(): void;
23
24
  private playChunk;
24
25
  playAgain(): Promise<void>;
25
26
  private monitorLoudness;
@@ -89,6 +89,16 @@ export class ChunkedAudioPlayer {
89
89
  this.shouldMonitorLoudness = false;
90
90
  cancelAnimationFrame(this.handle);
91
91
  }
92
+ cleanup() {
93
+ // Stop playback first
94
+ this.stopPlayback();
95
+ // Close AudioContext to free resources
96
+ if (this.audioContext && this.audioContext.state !== 'closed') {
97
+ this.audioContext.close().catch((e) => {
98
+ console.warn('Error closing AudioContext:', e);
99
+ });
100
+ }
101
+ }
92
102
  playChunk(chunk) {
93
103
  // console.log({queue: this.chunkQueue})
94
104
  if (!chunk) {
@@ -8,7 +8,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
- import { useState, useEffect } from 'react';
11
+ import { useState, useEffect, useRef } from 'react';
12
12
  import { FaPlayCircle, FaStopCircle } from 'react-icons/fa';
13
13
  import { useRimori } from '../../providers/PluginProvider';
14
14
  import { EventBus } from '@rimori/client';
@@ -20,6 +20,8 @@ export const AudioPlayer = ({ text, voice, language, hide, playListenerEvent, in
20
20
  const [isPlaying, setIsPlaying] = useState(false);
21
21
  const [isLoading, setIsLoading] = useState(false);
22
22
  const { ai } = useRimori();
23
+ const audioRef = useRef(null);
24
+ const eventBusListenerRef = useRef(null);
23
25
  useEffect(() => {
24
26
  if (audioUrl)
25
27
  setAudioUrl(null);
@@ -37,9 +39,22 @@ export const AudioPlayer = ({ text, voice, language, hide, playListenerEvent, in
37
39
  });
38
40
  // Effect to play audio when audioUrl changes and play state is true
39
41
  useEffect(() => {
40
- if (!audioUrl || !isPlaying)
42
+ if (!audioUrl || !isPlaying) {
43
+ // Stop any existing audio when not playing
44
+ if (audioRef.current) {
45
+ audioRef.current.pause();
46
+ audioRef.current.currentTime = 0;
47
+ audioRef.current = null;
48
+ }
41
49
  return;
50
+ }
51
+ // Clean up previous audio instance if it exists
52
+ if (audioRef.current) {
53
+ audioRef.current.pause();
54
+ audioRef.current.currentTime = 0;
55
+ }
42
56
  const audio = new Audio(audioUrl);
57
+ audioRef.current = audio;
43
58
  audio.playbackRate = speed;
44
59
  audio
45
60
  .play()
@@ -47,16 +62,41 @@ export const AudioPlayer = ({ text, voice, language, hide, playListenerEvent, in
47
62
  audio.onended = () => {
48
63
  setIsPlaying(false);
49
64
  isFetchingAudio = false;
65
+ audioRef.current = null;
50
66
  };
51
67
  })
52
68
  .catch((e) => {
53
69
  console.warn('Error playing audio:', e);
54
70
  setIsPlaying(false);
71
+ audioRef.current = null;
55
72
  });
56
73
  return () => {
57
- audio.pause();
74
+ if (audioRef.current) {
75
+ audioRef.current.pause();
76
+ audioRef.current.currentTime = 0;
77
+ audioRef.current = null;
78
+ }
58
79
  };
59
80
  }, [audioUrl, isPlaying, speed]);
81
+ // Cleanup on unmount - stop audio and revoke object URL
82
+ useEffect(() => {
83
+ return () => {
84
+ if (audioRef.current) {
85
+ audioRef.current.pause();
86
+ audioRef.current.currentTime = 0;
87
+ audioRef.current = null;
88
+ }
89
+ setIsPlaying(false);
90
+ };
91
+ }, []);
92
+ // Cleanup audioUrl on unmount
93
+ useEffect(() => {
94
+ return () => {
95
+ if (audioUrl) {
96
+ URL.revokeObjectURL(audioUrl);
97
+ }
98
+ };
99
+ }, [audioUrl]);
60
100
  const togglePlayback = () => {
61
101
  if (!isPlaying && !audioUrl) {
62
102
  generateAudio().then(() => setIsPlaying(true));
@@ -68,7 +108,15 @@ export const AudioPlayer = ({ text, voice, language, hide, playListenerEvent, in
68
108
  useEffect(() => {
69
109
  if (!playListenerEvent)
70
110
  return;
71
- EventBus.on(playListenerEvent, () => togglePlayback());
111
+ const handler = () => togglePlayback();
112
+ const listener = EventBus.on(playListenerEvent, handler);
113
+ eventBusListenerRef.current = listener;
114
+ return () => {
115
+ if (eventBusListenerRef.current) {
116
+ eventBusListenerRef.current.off();
117
+ eventBusListenerRef.current = null;
118
+ }
119
+ };
72
120
  }, [playListenerEvent]);
73
121
  useEffect(() => {
74
122
  if (!playOnMount || isFetchingAudio)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rimori/react-client",
3
- "version": "0.3.0-next.7",
3
+ "version": "0.3.0-next.8",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -23,7 +23,7 @@
23
23
  "format": "prettier --write ."
24
24
  },
25
25
  "peerDependencies": {
26
- "@rimori/client": "2.3.0-next.2",
26
+ "@rimori/client": "2.3.0-next.3",
27
27
  "react": "^18.1.0",
28
28
  "react-dom": "^18.1.0"
29
29
  },
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "devDependencies": {
36
36
  "@eslint/js": "^9.37.0",
37
- "@rimori/client": "2.3.0-next.2",
37
+ "@rimori/client": "2.3.0-next.3",
38
38
  "@types/react": "^18.3.21",
39
39
  "eslint-config-prettier": "^10.1.8",
40
40
  "eslint-plugin-prettier": "^5.5.4",
@@ -38,6 +38,13 @@ export function AssistantChat({ avatarImageUrl, voiceId, onComplete, autoStartCo
38
38
  }
39
39
  }, []);
40
40
 
41
+ // Cleanup: stop audio playback and clean up resources on unmount
42
+ useEffect(() => {
43
+ return () => {
44
+ sender.cleanup();
45
+ };
46
+ }, [sender]);
47
+
41
48
  useEffect(() => {
42
49
  let message = lastAssistantMessage;
43
50
  if (message !== messages[messages.length - 1]?.content) {
@@ -61,6 +61,13 @@ export function Avatar({
61
61
  }
62
62
  }, [autoStartConversation, voiceId]);
63
63
 
64
+ // Cleanup: stop audio playback and clean up resources on unmount
65
+ useEffect(() => {
66
+ return () => {
67
+ sender.cleanup();
68
+ };
69
+ }, [sender]);
70
+
64
71
  useEffect(() => {
65
72
  if (lastMessage?.role === 'assistant') {
66
73
  sender.handleNewText(lastMessage.content, isLoading);
@@ -74,6 +74,10 @@ export class MessageSender {
74
74
  this.player.stopPlayback();
75
75
  }
76
76
 
77
+ public cleanup() {
78
+ this.player.cleanup();
79
+ }
80
+
77
81
  private reset() {
78
82
  this.stop();
79
83
  this.fetchedSentences.clear();
@@ -88,6 +88,17 @@ export class ChunkedAudioPlayer {
88
88
  cancelAnimationFrame(this.handle);
89
89
  }
90
90
 
91
+ public cleanup(): void {
92
+ // Stop playback first
93
+ this.stopPlayback();
94
+ // Close AudioContext to free resources
95
+ if (this.audioContext && this.audioContext.state !== 'closed') {
96
+ this.audioContext.close().catch((e) => {
97
+ console.warn('Error closing AudioContext:', e);
98
+ });
99
+ }
100
+ }
101
+
91
102
  private playChunk(chunk: ArrayBuffer): Promise<void> {
92
103
  // console.log({queue: this.chunkQueue})
93
104
  if (!chunk) {
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect } from 'react';
1
+ import React, { useState, useEffect, useRef } from 'react';
2
2
  import { FaPlayCircle, FaStopCircle } from 'react-icons/fa';
3
3
  import { useRimori } from '../../providers/PluginProvider';
4
4
  import { EventBus } from '@rimori/client';
@@ -34,6 +34,8 @@ export const AudioPlayer: React.FC<AudioPlayerProps> = ({
34
34
  const [isPlaying, setIsPlaying] = useState(false);
35
35
  const [isLoading, setIsLoading] = useState(false);
36
36
  const { ai } = useRimori();
37
+ const audioRef = useRef<HTMLAudioElement | null>(null);
38
+ const eventBusListenerRef = useRef<{ off: () => void } | null>(null);
37
39
 
38
40
  useEffect(() => {
39
41
  if (audioUrl) setAudioUrl(null);
@@ -53,8 +55,24 @@ export const AudioPlayer: React.FC<AudioPlayerProps> = ({
53
55
 
54
56
  // Effect to play audio when audioUrl changes and play state is true
55
57
  useEffect(() => {
56
- if (!audioUrl || !isPlaying) return;
58
+ if (!audioUrl || !isPlaying) {
59
+ // Stop any existing audio when not playing
60
+ if (audioRef.current) {
61
+ audioRef.current.pause();
62
+ audioRef.current.currentTime = 0;
63
+ audioRef.current = null;
64
+ }
65
+ return;
66
+ }
67
+
68
+ // Clean up previous audio instance if it exists
69
+ if (audioRef.current) {
70
+ audioRef.current.pause();
71
+ audioRef.current.currentTime = 0;
72
+ }
73
+
57
74
  const audio = new Audio(audioUrl);
75
+ audioRef.current = audio;
58
76
  audio.playbackRate = speed;
59
77
  audio
60
78
  .play()
@@ -62,18 +80,45 @@ export const AudioPlayer: React.FC<AudioPlayerProps> = ({
62
80
  audio.onended = () => {
63
81
  setIsPlaying(false);
64
82
  isFetchingAudio = false;
83
+ audioRef.current = null;
65
84
  };
66
85
  })
67
86
  .catch((e) => {
68
87
  console.warn('Error playing audio:', e);
69
88
  setIsPlaying(false);
89
+ audioRef.current = null;
70
90
  });
71
91
 
72
92
  return () => {
73
- audio.pause();
93
+ if (audioRef.current) {
94
+ audioRef.current.pause();
95
+ audioRef.current.currentTime = 0;
96
+ audioRef.current = null;
97
+ }
74
98
  };
75
99
  }, [audioUrl, isPlaying, speed]);
76
100
 
101
+ // Cleanup on unmount - stop audio and revoke object URL
102
+ useEffect(() => {
103
+ return () => {
104
+ if (audioRef.current) {
105
+ audioRef.current.pause();
106
+ audioRef.current.currentTime = 0;
107
+ audioRef.current = null;
108
+ }
109
+ setIsPlaying(false);
110
+ };
111
+ }, []);
112
+
113
+ // Cleanup audioUrl on unmount
114
+ useEffect(() => {
115
+ return () => {
116
+ if (audioUrl) {
117
+ URL.revokeObjectURL(audioUrl);
118
+ }
119
+ };
120
+ }, [audioUrl]);
121
+
77
122
  const togglePlayback = () => {
78
123
  if (!isPlaying && !audioUrl) {
79
124
  generateAudio().then(() => setIsPlaying(true));
@@ -84,7 +129,16 @@ export const AudioPlayer: React.FC<AudioPlayerProps> = ({
84
129
 
85
130
  useEffect(() => {
86
131
  if (!playListenerEvent) return;
87
- EventBus.on(playListenerEvent, () => togglePlayback());
132
+ const handler = () => togglePlayback();
133
+ const listener = EventBus.on(playListenerEvent, handler);
134
+ eventBusListenerRef.current = listener;
135
+
136
+ return () => {
137
+ if (eventBusListenerRef.current) {
138
+ eventBusListenerRef.current.off();
139
+ eventBusListenerRef.current = null;
140
+ }
141
+ };
88
142
  }, [playListenerEvent]);
89
143
 
90
144
  useEffect(() => {