agora-appbuilder-core 4.0.35 → 4.1.0-beta-2
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/package.json +2 -2
- package/template/agora-rn-uikit/README.md +1 -40
- package/template/agora-rn-uikit/src/Contexts/PropsContext.tsx +1 -0
- package/template/agora-rn-uikit/src/Contexts/RtcContext.tsx +1 -0
- package/template/agora-rn-uikit/src/Reducer/Spotlight.ts +11 -0
- package/template/agora-rn-uikit/src/Reducer/index.ts +1 -0
- package/template/agora-rn-uikit/src/RtcConfigure.tsx +7 -0
- package/template/bridge/rtc/webNg/RtcEngine.ts +4 -1
- package/template/customization-api/app-state.ts +11 -7
- package/template/customization-api/{customize.ts → customize.tsx} +116 -11
- package/template/customization-api/sub-components.ts +4 -0
- package/template/customization-api/temp.ts +2 -0
- package/template/customization-api/typeDefinition.ts +2 -1
- package/template/customization-api/utils.ts +6 -1
- package/template/defaultConfig.js +4 -2
- package/template/global.d.ts +2 -0
- package/template/src/AppRoutes.tsx +15 -5
- package/template/src/ai-agent/components/AgentControls/AgentContext.tsx +163 -0
- package/template/src/ai-agent/components/AgentControls/LeaveCall.png +0 -0
- package/template/src/ai-agent/components/AgentControls/Vector.svg +3 -0
- package/template/src/ai-agent/components/AgentControls/const.ts +58 -0
- package/template/src/ai-agent/components/AgentControls/index.tsx +293 -0
- package/template/src/ai-agent/components/AudioVisualizer.tsx +91 -0
- package/template/src/ai-agent/components/Bottombar.tsx +91 -0
- package/template/src/ai-agent/components/CustomCreate.tsx +279 -0
- package/template/src/ai-agent/components/CustomCreateNative.tsx +265 -0
- package/template/src/ai-agent/components/CustomSidePanel.tsx +135 -0
- package/template/src/ai-agent/components/FallbackLogo.tsx +80 -0
- package/template/src/ai-agent/components/LocalAudioWave.tsx +171 -0
- package/template/src/ai-agent/components/agent-chat-panel/agent-chat-ui.tsx +82 -0
- package/template/src/ai-agent/components/icons.tsx +227 -0
- package/template/src/ai-agent/components/mobile/Bottombar.tsx +47 -0
- package/template/src/ai-agent/components/mobile/MobileLayoutComponent.tsx +106 -0
- package/template/src/ai-agent/components/mobile/Topbar.tsx +62 -0
- package/template/src/ai-agent/components/react-audio-visualize/LiveAudioVisualizer/LiveAudioVisualizer.tsx +173 -0
- package/template/src/ai-agent/components/react-audio-visualize/LiveAudioVisualizer/index.ts +1 -0
- package/template/src/ai-agent/components/react-audio-visualize/LiveAudioVisualizer/utils.ts +102 -0
- package/template/src/ai-agent/components/react-audio-visualize/index.ts +1 -0
- package/template/src/ai-agent/components/utils.ts +15 -0
- package/template/src/ai-agent/index.tsx +301 -0
- package/template/src/ai-agent/routes/CustomLoginRoute.tsx +25 -0
- package/template/src/ai-agent/routes/CustomValidateRoute.tsx +25 -0
- package/template/src/ai-agent/utils.ts +78 -0
- package/template/src/assets/font-styles.css +4 -0
- package/template/src/assets/fonts/icomoon.ttf +0 -0
- package/template/src/assets/selection.json +1 -1
- package/template/src/atoms/CustomIcon.tsx +1 -0
- package/template/src/atoms/ImageIcon.tsx +3 -0
- package/template/src/atoms/ToolbarItem.tsx +0 -2
- package/template/src/components/ChatContext.ts +7 -0
- package/template/src/components/Controls.tsx +6 -1
- package/template/src/components/ErrorBoundary.tsx +37 -0
- package/template/src/components/ErrorBoundaryFallback.tsx +44 -0
- package/template/src/components/RTMConfigure.tsx +25 -20
- package/template/src/components/participants/Participant.tsx +4 -0
- package/template/src/components/participants/UserActionMenuOptions.tsx +34 -1
- package/template/src/components/precall/PermissionHelper.tsx +11 -8
- package/template/src/language/default-labels/videoCallScreenLabels.ts +8 -0
- package/template/src/logger/AppBuilderLogger.tsx +4 -1
- package/template/src/pages/Create.tsx +11 -12
- package/template/src/pages/VideoCall.tsx +1 -0
- package/template/src/pages/video-call/ActionSheet.tsx +33 -29
- package/template/src/pages/video-call/SidePanelHeader.tsx +8 -3
- package/template/src/pages/video-call/SpotlightHighlighter.tsx +91 -0
- package/template/src/pages/video-call/VideoCallMobileView.tsx +17 -6
- package/template/src/pages/video-call/VideoCallScreen.tsx +0 -1
- package/template/src/pages/video-call/VideoRenderer.tsx +32 -4
- package/template/src/rtm-events/constants.ts +2 -0
- package/template/src/subComponents/ChatBubble.tsx +34 -15
- package/template/src/subComponents/FallbackLogo.tsx +3 -1
- package/template/src/subComponents/LocalAudioMute.tsx +20 -2
- package/template/src/utils/index.tsx +3 -4
- package/template/src/utils/useJoinRoom.ts +14 -0
- package/template/src/utils/useSpotlight.ts +31 -0
- package/template/tsconfig.json +23 -18
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import React, {useEffect, useState, useContext} from 'react';
|
|
2
|
+
import {ILocalAudioTrack, IRemoteAudioTrack} from 'agora-rtc-sdk-ng';
|
|
3
|
+
import {View, TouchableOpacity, Text} from 'react-native';
|
|
4
|
+
import {
|
|
5
|
+
MaxVideoView,
|
|
6
|
+
useContent,
|
|
7
|
+
useLocalUid,
|
|
8
|
+
type LayoutComponent,
|
|
9
|
+
useRtc,
|
|
10
|
+
useLocalAudio,
|
|
11
|
+
ToolbarPreset,
|
|
12
|
+
useEndCall,
|
|
13
|
+
useSidePanel,
|
|
14
|
+
ToolbarItem,
|
|
15
|
+
IconButton,
|
|
16
|
+
CustomizationApiInterface,
|
|
17
|
+
} from 'customization-api';
|
|
18
|
+
import {isMobileUA} from '../utils/common';
|
|
19
|
+
import AudioVisualizer, {DisconnectedView} from './components/AudioVisualizer';
|
|
20
|
+
import Bottombar from './components/Bottombar';
|
|
21
|
+
import CustomCreate from './components/CustomCreate';
|
|
22
|
+
import CustomCreateNative from './components/CustomCreateNative';
|
|
23
|
+
import {AudioVisualizerEffect} from './components/LocalAudioWave';
|
|
24
|
+
import MobileTopBar from './components/mobile/Topbar';
|
|
25
|
+
import MobileLayoutComponent from './components/mobile/MobileLayoutComponent';
|
|
26
|
+
import MobileBottombar from './components/mobile/Bottombar';
|
|
27
|
+
import {AgentProvider} from './components/AgentControls/AgentContext';
|
|
28
|
+
import {AgentContext} from './components/AgentControls/AgentContext';
|
|
29
|
+
import {AgentState} from './components/AgentControls/const';
|
|
30
|
+
import {
|
|
31
|
+
AGENT_PROXY_URL,
|
|
32
|
+
AGORA_SSO_LOGOUT_PATH,
|
|
33
|
+
AGORA_SSO_BASE,
|
|
34
|
+
} from './components/AgentControls/const';
|
|
35
|
+
import CustomSidePanel from './components/CustomSidePanel';
|
|
36
|
+
import CustomLoginRoute from './routes/CustomLoginRoute';
|
|
37
|
+
import CustomValidateRoute from './routes/CustomValidateRoute';
|
|
38
|
+
|
|
39
|
+
const Topbar = () => {
|
|
40
|
+
const {sidePanel, setSidePanel} = useSidePanel();
|
|
41
|
+
|
|
42
|
+
React.useEffect(() => {
|
|
43
|
+
setSidePanel('agent-transcript-panel');
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<ToolbarPreset
|
|
48
|
+
align="top"
|
|
49
|
+
items={{
|
|
50
|
+
'meeting-title': {hide: true},
|
|
51
|
+
'participant-count': {hide: true},
|
|
52
|
+
'recording-status': {hide: true},
|
|
53
|
+
chat: {hide: true},
|
|
54
|
+
participant: {hide: true},
|
|
55
|
+
settings: {hide: false},
|
|
56
|
+
agentTanscript: {
|
|
57
|
+
align: 'end',
|
|
58
|
+
component: () => {
|
|
59
|
+
const {agentAuthToken, setAgentAuthToken} =
|
|
60
|
+
useContext(AgentContext);
|
|
61
|
+
const isOpen = sidePanel === 'agent-transcript-panel';
|
|
62
|
+
|
|
63
|
+
const handlePress = () => {
|
|
64
|
+
setSidePanel(isOpen ? null : 'agent-transcript-panel');
|
|
65
|
+
};
|
|
66
|
+
return (
|
|
67
|
+
<ToolbarItem>
|
|
68
|
+
<IconButton
|
|
69
|
+
iconProps={{
|
|
70
|
+
name: 'chat-nav',
|
|
71
|
+
iconSize: 24,
|
|
72
|
+
tintColor: 'white',
|
|
73
|
+
iconBackgroundColor: isOpen
|
|
74
|
+
? $config.PRIMARY_ACTION_BRAND_COLOR
|
|
75
|
+
: $config.ICON_BG_COLOR,
|
|
76
|
+
}}
|
|
77
|
+
btnTextProps={{
|
|
78
|
+
textColor: 'white',
|
|
79
|
+
}}
|
|
80
|
+
onPress={handlePress}
|
|
81
|
+
/>
|
|
82
|
+
</ToolbarItem>
|
|
83
|
+
);
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
Logout: {
|
|
87
|
+
align: 'end',
|
|
88
|
+
component: () => {
|
|
89
|
+
const {agentAuthToken, setAgentAuthToken} =
|
|
90
|
+
useContext(AgentContext);
|
|
91
|
+
const endcall = useEndCall();
|
|
92
|
+
|
|
93
|
+
const ssoLogout = async () => {
|
|
94
|
+
const logoutUrl = `${AGENT_PROXY_URL}/logout`;
|
|
95
|
+
|
|
96
|
+
const response = await fetch(logoutUrl, {
|
|
97
|
+
headers: {
|
|
98
|
+
'Content-Type': 'application/json',
|
|
99
|
+
Authorization: `Bearer ${agentAuthToken}`,
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await endcall();
|
|
104
|
+
setAgentAuthToken(null);
|
|
105
|
+
|
|
106
|
+
const data = await response.json();
|
|
107
|
+
|
|
108
|
+
console.log({logoutdata: data});
|
|
109
|
+
|
|
110
|
+
return data;
|
|
111
|
+
};
|
|
112
|
+
const logout = async () => {
|
|
113
|
+
try {
|
|
114
|
+
// await ssoLogout()
|
|
115
|
+
const originURL = window.location.origin + '/create';
|
|
116
|
+
const frontend_redirect_creds = {
|
|
117
|
+
token: agentAuthToken,
|
|
118
|
+
frontend_redirect: originURL,
|
|
119
|
+
};
|
|
120
|
+
const REDIRECT_URL = `${AGENT_PROXY_URL}/logout?state=${JSON.stringify(
|
|
121
|
+
frontend_redirect_creds,
|
|
122
|
+
)}`;
|
|
123
|
+
const ssoUrl = `${AGORA_SSO_BASE}/${AGORA_SSO_LOGOUT_PATH}?redirect_uri=${REDIRECT_URL}`;
|
|
124
|
+
// console.log({REDIRECT_URL})
|
|
125
|
+
window.open(`${ssoUrl}`, '_self');
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.log({logoutFailed: error});
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
return null;
|
|
132
|
+
return (
|
|
133
|
+
<TouchableOpacity
|
|
134
|
+
style={{
|
|
135
|
+
display: 'flex',
|
|
136
|
+
height: 35,
|
|
137
|
+
padding: 20,
|
|
138
|
+
justifyContent: 'space-between',
|
|
139
|
+
alignItems: 'center',
|
|
140
|
+
gap: 8,
|
|
141
|
+
borderRadius: 4,
|
|
142
|
+
borderWidth: 1,
|
|
143
|
+
borderColor: '#00C2FF',
|
|
144
|
+
flexDirection: 'row',
|
|
145
|
+
}}
|
|
146
|
+
onPress={logout}>
|
|
147
|
+
<Text style={{color: '#FFF'}}>Logout</Text>
|
|
148
|
+
</TouchableOpacity>
|
|
149
|
+
);
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
}}
|
|
153
|
+
/>
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const DesktopLayoutComponent: LayoutComponent = () => {
|
|
158
|
+
const localUid = useLocalUid();
|
|
159
|
+
const {defaultContent, activeUids} = useContent();
|
|
160
|
+
const {RtcEngineUnsafe} = useRtc();
|
|
161
|
+
const [localTracks, setLocalTrack] = useState<ILocalAudioTrack | null>(null);
|
|
162
|
+
const [remoteTrack, setRemoteTrack] = useState<IRemoteAudioTrack | null>(
|
|
163
|
+
null,
|
|
164
|
+
);
|
|
165
|
+
const [mediaStreamTrack, setMediaStreamTrack] =
|
|
166
|
+
React.useState<MediaStreamTrack>();
|
|
167
|
+
|
|
168
|
+
const {agentConnectionState, setAgentConnectionState, agentUID} =
|
|
169
|
+
useContext(AgentContext);
|
|
170
|
+
|
|
171
|
+
const {getLocalAudioStream, getRemoteAudioStream} = useLocalAudio();
|
|
172
|
+
|
|
173
|
+
const connected = activeUids.includes(agentUID);
|
|
174
|
+
console.log({activeUids}, 'active uids');
|
|
175
|
+
|
|
176
|
+
// this state occurs when agent_stop is successful, but
|
|
177
|
+
// user is still not disconnected from the RTC channel - state-of-wait
|
|
178
|
+
const isAwaitingLeave = agentConnectionState === AgentState.AWAITING_LEAVE;
|
|
179
|
+
console.log(
|
|
180
|
+
{isAwaitingLeave},
|
|
181
|
+
{connected},
|
|
182
|
+
'what is going on',
|
|
183
|
+
agentConnectionState,
|
|
184
|
+
);
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
if (getLocalAudioStream()) {
|
|
187
|
+
setLocalTrack(getLocalAudioStream());
|
|
188
|
+
}
|
|
189
|
+
}, [RtcEngineUnsafe]);
|
|
190
|
+
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
if (getRemoteAudioStream(agentUID)) {
|
|
193
|
+
setRemoteTrack(getRemoteAudioStream(agentUID));
|
|
194
|
+
}
|
|
195
|
+
}, [activeUids]);
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<View
|
|
199
|
+
style={{
|
|
200
|
+
flex: 1,
|
|
201
|
+
display: 'flex',
|
|
202
|
+
flexDirection: 'column',
|
|
203
|
+
borderRadius: 10,
|
|
204
|
+
}}>
|
|
205
|
+
<MaxVideoView
|
|
206
|
+
user={{
|
|
207
|
+
...defaultContent[agentUID],
|
|
208
|
+
name: 'OpenAI',
|
|
209
|
+
video: false,
|
|
210
|
+
}}
|
|
211
|
+
CustomChild={() =>
|
|
212
|
+
// show agent voice waves, when agent is connected to the channel, but also not on a state-of-wait,
|
|
213
|
+
connected && !isAwaitingLeave ? (
|
|
214
|
+
<AudioVisualizer audioTrack={remoteTrack} />
|
|
215
|
+
) : (
|
|
216
|
+
<DisconnectedView isConnected={connected} />
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
hideMenuOptions={true}
|
|
220
|
+
/>
|
|
221
|
+
<View
|
|
222
|
+
style={{
|
|
223
|
+
position: 'absolute',
|
|
224
|
+
bottom: 10,
|
|
225
|
+
right: 10,
|
|
226
|
+
display: 'flex',
|
|
227
|
+
flexDirection: 'row',
|
|
228
|
+
height: 200,
|
|
229
|
+
width: 300,
|
|
230
|
+
}}>
|
|
231
|
+
<MaxVideoView user={defaultContent[localUid]} hideMenuOptions={true} />
|
|
232
|
+
<View
|
|
233
|
+
style={{
|
|
234
|
+
position: 'absolute',
|
|
235
|
+
bottom: 16,
|
|
236
|
+
right: 10,
|
|
237
|
+
}}>
|
|
238
|
+
{localTracks && (
|
|
239
|
+
<AudioVisualizerEffect
|
|
240
|
+
type="user"
|
|
241
|
+
barWidth={3}
|
|
242
|
+
minBarHeight={2}
|
|
243
|
+
maxBarHeight={25}
|
|
244
|
+
audioTrack={localTracks}
|
|
245
|
+
borderRadius={2}
|
|
246
|
+
gap={4}
|
|
247
|
+
/>
|
|
248
|
+
)}
|
|
249
|
+
</View>
|
|
250
|
+
</View>
|
|
251
|
+
</View>
|
|
252
|
+
);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
export const AI_AGENT_CUSTOMIZATION: CustomizationApiInterface = {
|
|
256
|
+
components: {
|
|
257
|
+
appRoot: AgentProvider,
|
|
258
|
+
create: isMobileUA() ? CustomCreateNative : CustomCreate,
|
|
259
|
+
//preferenceWrapper: AgentProvider,
|
|
260
|
+
videoCall: {
|
|
261
|
+
customLayout() {
|
|
262
|
+
return [
|
|
263
|
+
{
|
|
264
|
+
name: 'Ai-Agent',
|
|
265
|
+
label: 'Ai-Agent',
|
|
266
|
+
icon: '🤖',
|
|
267
|
+
component: isMobileUA()
|
|
268
|
+
? MobileLayoutComponent
|
|
269
|
+
: DesktopLayoutComponent,
|
|
270
|
+
},
|
|
271
|
+
];
|
|
272
|
+
},
|
|
273
|
+
customSidePanel: () => {
|
|
274
|
+
return [
|
|
275
|
+
{
|
|
276
|
+
name: 'agent-transcript-panel',
|
|
277
|
+
component: CustomSidePanel,
|
|
278
|
+
title: 'Agent Transcript',
|
|
279
|
+
onClose: () => {},
|
|
280
|
+
},
|
|
281
|
+
];
|
|
282
|
+
},
|
|
283
|
+
topToolBar: isMobileUA() ? MobileTopBar : Topbar,
|
|
284
|
+
bottomToolBar: isMobileUA() ? MobileBottombar : Bottombar,
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
customRoutes: [
|
|
288
|
+
{
|
|
289
|
+
component: CustomLoginRoute,
|
|
290
|
+
exact: true,
|
|
291
|
+
path: '/login',
|
|
292
|
+
isTopLevelRoute: true,
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
component: CustomValidateRoute,
|
|
296
|
+
exact: true,
|
|
297
|
+
path: '/validate',
|
|
298
|
+
isTopLevelRoute: true,
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React, {useEffect} from 'react';
|
|
2
|
+
import {Loading} from 'customization-api';
|
|
3
|
+
import {
|
|
4
|
+
AGENT_PROXY_URL,
|
|
5
|
+
AGORA_SSO_BASE,
|
|
6
|
+
AGORA_SSO_LOGIN_PATH,
|
|
7
|
+
AGORA_SSO_CLIENT_ID,
|
|
8
|
+
} from '../components/AgentControls/const';
|
|
9
|
+
|
|
10
|
+
export default function CustomLoginRoute() {
|
|
11
|
+
const handleSSOLogin = () => {
|
|
12
|
+
const REDIRECT_URL = `${AGENT_PROXY_URL}/login`;
|
|
13
|
+
const originURL = window.location.origin + '/validate';
|
|
14
|
+
|
|
15
|
+
const ssoUrl = `${AGORA_SSO_BASE}/${AGORA_SSO_LOGIN_PATH}?scope=basic_info&response_type=code&state=url=${originURL}&redirect_uri=${REDIRECT_URL}&client_id=${AGORA_SSO_CLIENT_ID}`;
|
|
16
|
+
console.log('sso', ssoUrl);
|
|
17
|
+
window.location.href = ssoUrl;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
handleSSOLogin();
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
return <Loading text="Redirecting to agora SSO..." />;
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React, {useContext, useEffect} from 'react';
|
|
2
|
+
import {useHistory, Loading} from 'customization-api';
|
|
3
|
+
import {AgentContext} from '../components/AgentControls/AgentContext';
|
|
4
|
+
|
|
5
|
+
const CustomValidateRoute = () => {
|
|
6
|
+
const history = useHistory();
|
|
7
|
+
const {setAgentAuthToken} = useContext(AgentContext);
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const queryParams = new URLSearchParams(window.location.search);
|
|
11
|
+
queryParams.forEach((value, key) => {
|
|
12
|
+
console.log(`${key}: ${value}`);
|
|
13
|
+
});
|
|
14
|
+
// check for success login from url param and redirect to create using custom param
|
|
15
|
+
|
|
16
|
+
if (queryParams.has('token')) {
|
|
17
|
+
// Redirect to create using custom param only if token exists
|
|
18
|
+
setAgentAuthToken(queryParams.get('token') || '');
|
|
19
|
+
history.push('/create?auth=success');
|
|
20
|
+
}
|
|
21
|
+
}, []);
|
|
22
|
+
return <Loading text="" />;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default CustomValidateRoute;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {IMicrophoneAudioTrack} from 'agora-rtc-sdk-ng';
|
|
2
|
+
import {useState, useEffect} from 'react';
|
|
3
|
+
|
|
4
|
+
export const useMultibandTrackVolume = (
|
|
5
|
+
track?: IMicrophoneAudioTrack | MediaStreamTrack,
|
|
6
|
+
bands: number = 5,
|
|
7
|
+
loPass: number = 100,
|
|
8
|
+
hiPass: number = 600,
|
|
9
|
+
) => {
|
|
10
|
+
const [frequencyBands, setFrequencyBands] = useState<Float32Array[]>([]);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (!track) {
|
|
14
|
+
return setFrequencyBands(new Array(bands).fill(new Float32Array(0)));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const ctx = new AudioContext();
|
|
18
|
+
let finTrack =
|
|
19
|
+
track instanceof MediaStreamTrack ? track : track.getMediaStreamTrack();
|
|
20
|
+
const mediaStream = new MediaStream([finTrack]);
|
|
21
|
+
const source = ctx.createMediaStreamSource(mediaStream);
|
|
22
|
+
const analyser = ctx.createAnalyser();
|
|
23
|
+
analyser.fftSize = 2048;
|
|
24
|
+
|
|
25
|
+
source.connect(analyser);
|
|
26
|
+
|
|
27
|
+
const bufferLength = analyser.frequencyBinCount;
|
|
28
|
+
const dataArray = new Float32Array(bufferLength);
|
|
29
|
+
|
|
30
|
+
const updateVolume = () => {
|
|
31
|
+
analyser.getFloatFrequencyData(dataArray);
|
|
32
|
+
let frequencies: Float32Array = new Float32Array(dataArray.length);
|
|
33
|
+
for (let i = 0; i < dataArray.length; i++) {
|
|
34
|
+
frequencies[i] = dataArray[i];
|
|
35
|
+
}
|
|
36
|
+
frequencies = frequencies.slice(loPass, hiPass);
|
|
37
|
+
|
|
38
|
+
const normalizedFrequencies = normalizeFrequencies(frequencies);
|
|
39
|
+
const chunkSize = Math.ceil(normalizedFrequencies.length / bands);
|
|
40
|
+
const chunks: Float32Array[] = [];
|
|
41
|
+
for (let i = 0; i < bands; i++) {
|
|
42
|
+
chunks.push(
|
|
43
|
+
normalizedFrequencies.slice(i * chunkSize, (i + 1) * chunkSize),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
setFrequencyBands(chunks);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const interval = setInterval(updateVolume, 10);
|
|
51
|
+
|
|
52
|
+
return () => {
|
|
53
|
+
source.disconnect();
|
|
54
|
+
clearInterval(interval);
|
|
55
|
+
};
|
|
56
|
+
}, [track, loPass, hiPass, bands]);
|
|
57
|
+
|
|
58
|
+
return frequencyBands;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const normalizeFrequencies = (frequencies: Float32Array) => {
|
|
62
|
+
const normalizeDb = (value: number) => {
|
|
63
|
+
const minDb = -100;
|
|
64
|
+
const maxDb = -10;
|
|
65
|
+
let db = 1 - (Math.max(minDb, Math.min(maxDb, value)) * -1) / 100;
|
|
66
|
+
db = Math.sqrt(db);
|
|
67
|
+
|
|
68
|
+
return db;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Normalize all frequency values
|
|
72
|
+
return frequencies.map(value => {
|
|
73
|
+
if (value === -Infinity) {
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
return normalizeDb(value);
|
|
77
|
+
});
|
|
78
|
+
};
|
|
Binary file
|