@vanira/sdk-react-native 0.0.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/README.md +239 -0
- package/package.json +53 -0
- package/src/__tests__/WebRTCClient.integration.test.ts +396 -0
- package/src/__tests__/adapters.test.ts +475 -0
- package/src/__tests__/httpResponse.test.ts +25 -0
- package/src/__tests__/mocks/react-native-incall-manager.ts +8 -0
- package/src/__tests__/mocks/react-native-permissions.ts +15 -0
- package/src/__tests__/mocks/react-native-webrtc.ts +6 -0
- package/src/__tests__/mocks/react-native.ts +28 -0
- package/src/__tests__/preset.test.ts +239 -0
- package/src/__tests__/resolveRuntimeConfig.test.ts +90 -0
- package/src/__tests__/storage.test.ts +211 -0
- package/src/__tests__/webrtcSignaling.test.ts +42 -0
- package/src/adapters/PeerConnectionAdapter.ts +101 -0
- package/src/adapters/browser/BrowserAudioAdapter.ts +43 -0
- package/src/adapters/browser/BrowserDataChannelAdapter.ts +69 -0
- package/src/adapters/browser/BrowserMediaAdapter.ts +15 -0
- package/src/adapters/browser/BrowserPeerAdapter.ts +14 -0
- package/src/adapters/browser/index.ts +4 -0
- package/src/adapters/interfaces.ts +84 -0
- package/src/adapters/react-native/RNAudioAdapter.ts +42 -0
- package/src/adapters/react-native/RNDataChannelAdapter.ts +79 -0
- package/src/adapters/react-native/RNMediaAdapter.ts +46 -0
- package/src/adapters/react-native/RNPeerAdapter.ts +28 -0
- package/src/adapters/react-native/callAudioRouting.ts +115 -0
- package/src/adapters/react-native/decodeUtf8.ts +72 -0
- package/src/adapters/react-native/index.ts +4 -0
- package/src/adapters/react-native/rnUploadFile.ts +76 -0
- package/src/adapters/storage/BrowserDualStorageAdapter.ts +71 -0
- package/src/adapters/storage/MemoryStorageAdapter.ts +50 -0
- package/src/adapters/storage/StorageAdapter.ts +21 -0
- package/src/adapters/storage/createSyncStorageAdapter.ts +40 -0
- package/src/adapters/storage/index.ts +7 -0
- package/src/api/services/ChatService.ts +304 -0
- package/src/api/services/ConfigService.ts +33 -0
- package/src/assets/icons.js +35 -0
- package/src/cdn.ts +68 -0
- package/src/core/CallSessionStore.ts +137 -0
- package/src/core/DraggableController.ts +83 -0
- package/src/core/SessionManager.ts +322 -0
- package/src/core/VaniraAI.ts +464 -0
- package/src/core/WebRTCClient.ts +1012 -0
- package/src/core/httpResponse.ts +22 -0
- package/src/core/iceServers.ts +18 -0
- package/src/core/toolCallNormalize.ts +80 -0
- package/src/core/voice-client.js +236 -0
- package/src/core/webrtcSignaling.ts +72 -0
- package/src/index.js +34 -0
- package/src/index.ts +6 -0
- package/src/platforms/browser.ts +67 -0
- package/src/platforms/react-native.ts +105 -0
- package/src/presets/BookingCalendarModal.tsx +457 -0
- package/src/presets/CameraModal.tsx +576 -0
- package/src/presets/DynamicFormModal.tsx +378 -0
- package/src/presets/NativePresetRenderer.tsx +350 -0
- package/src/presets/NavigateHandler.tsx +75 -0
- package/src/presets/PresetHost.tsx +155 -0
- package/src/presets/PresetShellModal.tsx +97 -0
- package/src/presets/UploadModal.tsx +321 -0
- package/src/presets/calendar/calendarUtils.ts +386 -0
- package/src/presets/call/CallSpeakerToggle.tsx +59 -0
- package/src/presets/call/callAudioRouting.ts +2 -0
- package/src/presets/call/useCallSpeaker.ts +31 -0
- package/src/presets/camera/cameraPermissions.ts +18 -0
- package/src/presets/camera/cameraStream.ts +19 -0
- package/src/presets/camera/cameraUtils.ts +21 -0
- package/src/presets/camera/useLivenessFlow.ts +95 -0
- package/src/presets/chalkboard/ChalkboardOverlay.tsx +156 -0
- package/src/presets/chalkboard/EraseTextHandler.tsx +95 -0
- package/src/presets/chalkboard/TypeTextHandler.tsx +107 -0
- package/src/presets/chalkboard/boardAbort.ts +36 -0
- package/src/presets/chalkboard/boardQueue.ts +620 -0
- package/src/presets/chalkboard/chalkboardSession.ts +75 -0
- package/src/presets/chalkboard/drawUtils.ts +123 -0
- package/src/presets/chalkboard/textUtils.ts +109 -0
- package/src/presets/clipRegion/ClipRegionModal.tsx +261 -0
- package/src/presets/clipRegion/clipRegionBridge.ts +19 -0
- package/src/presets/form/formValidation.ts +104 -0
- package/src/presets/form/parseFormFields.ts +171 -0
- package/src/presets/host/HostElementPresetHandler.tsx +155 -0
- package/src/presets/host/hostPresetBridge.ts +71 -0
- package/src/presets/index.ts +63 -0
- package/src/presets/liveScreen/CloseLiveScreenHandler.tsx +36 -0
- package/src/presets/liveScreen/LiveScreenCaptureHost.tsx +312 -0
- package/src/presets/liveScreen/LiveScreenHandler.tsx +25 -0
- package/src/presets/liveScreen/LiveScreenPipOverlay.tsx +6 -0
- package/src/presets/liveScreen/liveScreenSession.ts +73 -0
- package/src/presets/liveVision/CloseLiveVisionHandler.tsx +29 -0
- package/src/presets/liveVision/LiveVisionCameraHost.tsx +317 -0
- package/src/presets/liveVision/LiveVisionHandler.tsx +26 -0
- package/src/presets/liveVision/LiveVisionPipOverlay.tsx +7 -0
- package/src/presets/liveVision/liveVisionFrameLoop.ts +38 -0
- package/src/presets/liveVision/liveVisionSession.ts +75 -0
- package/src/presets/liveVision/liveVisionUpload.ts +62 -0
- package/src/presets/navigation/internalRouteRegistry.ts +25 -0
- package/src/presets/navigation/navigationBridge.ts +76 -0
- package/src/presets/navigation/navigationTypes.ts +12 -0
- package/src/presets/parseToolCall.ts +60 -0
- package/src/presets/presetClientAdapter.ts +29 -0
- package/src/presets/presetCompletion.ts +91 -0
- package/src/presets/presetEventHelpers.ts +45 -0
- package/src/presets/registry.ts +128 -0
- package/src/presets/streaming/mediaFrameUpload.ts +93 -0
- package/src/presets/types.ts +74 -0
- package/src/presets/upload/pickUploadFile.ts +256 -0
- package/src/presets/upload/uploadFormats.ts +163 -0
- package/src/presets/upload/uploadUtils.ts +68 -0
- package/src/react/PresetRenderer.tsx +144 -0
- package/src/react/index.ts +1 -0
- package/src/runtime/browserRuntime.ts +54 -0
- package/src/runtime/platform.ts +17 -0
- package/src/runtime/reactNativeRuntime.ts +68 -0
- package/src/runtime/resolveRuntimeConfig.ts +75 -0
- package/src/runtime/runtimeBundles.ts +74 -0
- package/src/runtime/types.ts +135 -0
- package/src/types/react-native-incall-manager.d.ts +17 -0
- package/src/types/react-native-webrtc.d.ts +47 -0
- package/src/types.ts +133 -0
- package/src/ui/VaniraWidget.ts +87 -0
- package/src/ui/abstraction/AbstractWidgetProvider.ts +18 -0
- package/src/ui/abstraction/interfaces.ts +12 -0
- package/src/ui/adapters/VaniraChatAdapter.ts +42 -0
- package/src/ui/components/AvatarView.ts +81 -0
- package/src/ui/components/ChatWindow.ts +263 -0
- package/src/ui/components/FloatingButton.ts +163 -0
- package/src/ui/components/FloatingWelcomeChips.ts +137 -0
- package/src/ui/components/Panel.ts +120 -0
- package/src/ui/components/VoiceOrb.ts +79 -0
- package/src/ui/components/VoiceOverlay.ts +497 -0
- package/src/ui/components/index.ts +7 -0
- package/src/ui/factory/WidgetFactory.ts +16 -0
- package/src/ui/icons_data.ts +2 -0
- package/src/ui/presets/WidgetPresetRenderer.ts +1802 -0
- package/src/ui/presets/types.ts +16 -0
- package/src/ui/providers/VaniraInternalProvider.ts +1066 -0
- package/src/ui/styles/index.ts +323 -0
- package/src/ui/styles/keyframes.ts +76 -0
- package/src/ui/styles/theme.ts +57 -0
- package/src/ui/styles/widget.css.ts +838 -0
- package/src/ui/utils.ts +37 -0
- package/src/ui/views/AbstractChatView.ts +93 -0
- package/src/ui/views/AbstractVoiceView.ts +57 -0
- package/src/ui/views/AvatarOnlyView.ts +78 -0
- package/src/ui/views/ChatAvatarView.ts +66 -0
- package/src/ui/views/ChatOnlyView.ts +28 -0
- package/src/ui/views/ChatVoiceView.ts +15 -0
- package/src/ui/views/VoiceOnlyView.ts +25 -0
- package/src/ui/views/index.ts +5 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { AudioAdapter, AudioPlayerHandle } from '../interfaces';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Browser implementation of AudioPlayerHandle.
|
|
5
|
+
* Wraps HTMLAudioElement to play the incoming remote MediaStream.
|
|
6
|
+
*/
|
|
7
|
+
class BrowserAudioPlayerHandle implements AudioPlayerHandle {
|
|
8
|
+
private el: HTMLAudioElement;
|
|
9
|
+
|
|
10
|
+
constructor(stream: MediaStream) {
|
|
11
|
+
this.el = new Audio();
|
|
12
|
+
this.el.srcObject = stream;
|
|
13
|
+
this.el.play().catch(e =>
|
|
14
|
+
console.warn('[BrowserAudio] Autoplay blocked — user gesture may be required:', e)
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
pause(): void {
|
|
19
|
+
// NOTE: Do NOT pause a live WebRTC audio track element.
|
|
20
|
+
// Pausing a MediaStream-backed audio element mutes all future packets permanently.
|
|
21
|
+
// clearAudio from the server means the server stops transmitting — no client action needed.
|
|
22
|
+
// This is kept here as a no-op to satisfy the interface without breaking the stream.
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
cleanup(): void {
|
|
26
|
+
this.el.pause();
|
|
27
|
+
this.el.srcObject = null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
onEnded(callback: () => void): void {
|
|
31
|
+
this.el.onended = callback;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Browser AudioAdapter.
|
|
37
|
+
* Creates an HTMLAudioElement-backed player for the incoming remote audio track.
|
|
38
|
+
*/
|
|
39
|
+
export class BrowserAudioAdapter implements AudioAdapter {
|
|
40
|
+
createRemotePlayer(stream: MediaStream): AudioPlayerHandle {
|
|
41
|
+
return new BrowserAudioPlayerHandle(stream);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DataChannelAdapter,
|
|
3
|
+
DataChannelController,
|
|
4
|
+
DataChannelHandlers,
|
|
5
|
+
} from '../PeerConnectionAdapter';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Browser DataChannelAdapter.
|
|
9
|
+
*
|
|
10
|
+
* Normalises browser's three wire formats (string / ArrayBuffer / Blob) to
|
|
11
|
+
* decoded UTF-8 text before invoking onMessage.
|
|
12
|
+
*
|
|
13
|
+
* Binary frame policy — preserved from original WebRTCClient behaviour:
|
|
14
|
+
* Only binary frames that decode to JSON with event === 'client_tool_call'
|
|
15
|
+
* are forwarded. All other binary frames are logged but dropped to prevent
|
|
16
|
+
* false clearAudio events from audio packet payloads.
|
|
17
|
+
*/
|
|
18
|
+
export class BrowserDataChannelAdapter implements DataChannelAdapter {
|
|
19
|
+
bind(channel: RTCDataChannel, handlers: DataChannelHandlers): DataChannelController {
|
|
20
|
+
channel.onopen = () => handlers.onOpen();
|
|
21
|
+
channel.onerror = (e) => handlers.onError(e);
|
|
22
|
+
|
|
23
|
+
channel.onmessage = (e: MessageEvent) => {
|
|
24
|
+
if (typeof e.data === 'string') {
|
|
25
|
+
handlers.onMessage({ text: e.data });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (e.data instanceof ArrayBuffer) {
|
|
30
|
+
try {
|
|
31
|
+
const text = new TextDecoder().decode(e.data);
|
|
32
|
+
const parsed = JSON.parse(text) as { event?: string };
|
|
33
|
+
if (parsed?.event === 'client_tool_call') {
|
|
34
|
+
console.log('[DataChannel] Binary client_tool_call decoded and forwarded');
|
|
35
|
+
handlers.onMessage({ text });
|
|
36
|
+
} else {
|
|
37
|
+
console.log('[DataChannel] Binary frame dropped (not client_tool_call):', parsed?.event);
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
console.log('[DataChannel] Non-decodable binary frame:',
|
|
41
|
+
(e.data as ArrayBuffer).byteLength, 'bytes — dropped');
|
|
42
|
+
}
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (e.data instanceof Blob) {
|
|
47
|
+
e.data.text().then(text => {
|
|
48
|
+
handlers.onMessage({ text });
|
|
49
|
+
}).catch(err => {
|
|
50
|
+
console.warn('[DataChannel] Failed to read Blob message:', err);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
send(text: string): void {
|
|
57
|
+
if (channel.readyState === 'open') {
|
|
58
|
+
channel.send(text);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
isOpen(): boolean {
|
|
62
|
+
return channel.readyState === 'open';
|
|
63
|
+
},
|
|
64
|
+
close(): void {
|
|
65
|
+
channel.close();
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { MediaAdapter, MediaAudioConstraints } from '../interfaces';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Browser MediaAdapter.
|
|
5
|
+
* Delegates directly to navigator.mediaDevices — available in all modern browsers.
|
|
6
|
+
*/
|
|
7
|
+
export class BrowserMediaAdapter implements MediaAdapter {
|
|
8
|
+
async getUserAudio(constraints: MediaAudioConstraints): Promise<MediaStream> {
|
|
9
|
+
return navigator.mediaDevices.getUserMedia({ audio: constraints as MediaTrackConstraints });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
stopStream(stream: MediaStream): void {
|
|
13
|
+
stream.getTracks().forEach(track => track.stop());
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PeerConnectionAdapter, PeerConnectionConfig } from '../PeerConnectionAdapter';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Browser PeerConnectionAdapter.
|
|
5
|
+
* Wraps `new RTCPeerConnection(config)` so the core never instantiates it directly.
|
|
6
|
+
*/
|
|
7
|
+
export class BrowserPeerAdapter implements PeerConnectionAdapter {
|
|
8
|
+
create(config: PeerConnectionConfig): RTCPeerConnection {
|
|
9
|
+
return new RTCPeerConnection({
|
|
10
|
+
iceServers: config.iceServers as RTCIceServer[],
|
|
11
|
+
iceTransportPolicy: config.iceTransportPolicy ?? 'all',
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform adapter interfaces for the Vanira SDK.
|
|
3
|
+
*
|
|
4
|
+
* The core session (WebRTCClient / VaniraAI) depends only on these interfaces.
|
|
5
|
+
* Platform-specific implementations live in:
|
|
6
|
+
* adapters/browser/ — Web / browser
|
|
7
|
+
* adapters/react-native/ — React Native (react-native-webrtc)
|
|
8
|
+
*
|
|
9
|
+
* Future:
|
|
10
|
+
* flutter-sdk/lib/src/adapters/ — Dart implementations for Flutter
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// ─── Audio constraints passed to the media adapter ───────────────────────────
|
|
14
|
+
|
|
15
|
+
export interface MediaAudioConstraints {
|
|
16
|
+
echoCancellation?: boolean;
|
|
17
|
+
noiseSuppression?: boolean;
|
|
18
|
+
autoGainControl?: boolean;
|
|
19
|
+
/** Accepts a number or an ideal-hint object */
|
|
20
|
+
sampleRate?: number | { ideal: number };
|
|
21
|
+
channelCount?: number | { ideal: number };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ─── AudioPlayerHandle ────────────────────────────────────────────────────────
|
|
25
|
+
//
|
|
26
|
+
// Returned by AudioAdapter.createRemotePlayer(). Core session code holds a
|
|
27
|
+
// reference and calls these methods to control playback.
|
|
28
|
+
|
|
29
|
+
export interface AudioPlayerHandle {
|
|
30
|
+
/**
|
|
31
|
+
* Stop current playback and reset position.
|
|
32
|
+
* Called when the server sends a `clearAudio` event or on action interrupt.
|
|
33
|
+
*/
|
|
34
|
+
pause(): void;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Release all resources tied to this player.
|
|
38
|
+
* Called during disconnect() cleanup.
|
|
39
|
+
*/
|
|
40
|
+
cleanup(): void;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Register a callback to fire when natural playback ends.
|
|
44
|
+
* The core session uses this to send the `playedStream` event back to the server.
|
|
45
|
+
*
|
|
46
|
+
* NOTE — React Native: this callback is never invoked because react-native-webrtc
|
|
47
|
+
* routes audio automatically through the native stack. The RNAudioAdapter stubs
|
|
48
|
+
* this out and logs a warning. The server must not rely on receiving `playedStream`
|
|
49
|
+
* from RN clients; use a server-side estimated-duration fallback instead.
|
|
50
|
+
*/
|
|
51
|
+
onEnded(callback: () => void): void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── AudioAdapter ─────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
export interface AudioAdapter {
|
|
57
|
+
/**
|
|
58
|
+
* Attach the incoming remote MediaStream to an audio output and start playing.
|
|
59
|
+
* Returns a handle the session holds for later pause / cleanup calls.
|
|
60
|
+
*
|
|
61
|
+
* Browser: creates an HTMLAudioElement, sets srcObject, calls play().
|
|
62
|
+
* React Native: no-op — react-native-webrtc routes audio to the native speaker
|
|
63
|
+
* automatically via the peer connection. Returns a stub handle.
|
|
64
|
+
*/
|
|
65
|
+
createRemotePlayer(stream: MediaStream): AudioPlayerHandle;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─── MediaAdapter ─────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
export interface MediaAdapter {
|
|
71
|
+
/**
|
|
72
|
+
* Request the user's microphone.
|
|
73
|
+
*
|
|
74
|
+
* Browser: navigator.mediaDevices.getUserMedia({ audio: constraints })
|
|
75
|
+
* React Native: react-native-webrtc mediaDevices.getUserMedia (same API, polyfilled)
|
|
76
|
+
* Flutter: flutter_webrtc navigator.mediaDevices.getUserMedia (Dart, same shape)
|
|
77
|
+
*/
|
|
78
|
+
getUserAudio(constraints: MediaAudioConstraints): Promise<MediaStream>;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Stop all tracks on a stream, releasing the underlying hardware device.
|
|
82
|
+
*/
|
|
83
|
+
stopStream(stream: MediaStream): void;
|
|
84
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { AudioAdapter, AudioPlayerHandle } from '../interfaces';
|
|
2
|
+
import { activateCallAudioPlayback } from './callAudioRouting';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* React Native AudioPlayerHandle — no HTMLAudioElement equivalent.
|
|
6
|
+
* Remote audio is routed by react-native-webrtc + InCallManager.
|
|
7
|
+
*/
|
|
8
|
+
class RNAudioPlayerHandle implements AudioPlayerHandle {
|
|
9
|
+
pause(): void {
|
|
10
|
+
// clearAudio means the server stops transmitting — do not pause the track.
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
cleanup(): void {
|
|
14
|
+
// react-native-webrtc cleans up when RTCPeerConnection closes.
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
onEnded(_callback: () => void): void {
|
|
18
|
+
// RN has no audio onended — WebRTCClient sends playedStream after final transcription.
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* React Native AudioAdapter.
|
|
24
|
+
* Starts in-call audio routing and enables remote audio tracks for playback.
|
|
25
|
+
*/
|
|
26
|
+
export class RNAudioAdapter implements AudioAdapter {
|
|
27
|
+
createRemotePlayer(stream: MediaStream): AudioPlayerHandle {
|
|
28
|
+
activateCallAudioPlayback();
|
|
29
|
+
|
|
30
|
+
const tracks = stream.getTracks?.() ?? [];
|
|
31
|
+
for (const track of tracks) {
|
|
32
|
+
if (track.kind === 'audio') {
|
|
33
|
+
track.enabled = true;
|
|
34
|
+
console.log(
|
|
35
|
+
`[RNAudio] Remote audio track enabled (id=${track.id}, state=${track.readyState})`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return new RNAudioPlayerHandle();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DataChannelAdapter,
|
|
3
|
+
DataChannelController,
|
|
4
|
+
DataChannelHandlers,
|
|
5
|
+
} from '../PeerConnectionAdapter';
|
|
6
|
+
import { decodeUtf8ArrayBuffer, toArrayBuffer } from './decodeUtf8';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* React Native DataChannelAdapter.
|
|
10
|
+
*
|
|
11
|
+
* react-native-webrtc usually delivers string frames; on Android, client_tool_call
|
|
12
|
+
* may arrive as ArrayBuffer. Must not use TextDecoder directly — Hermes may lack it.
|
|
13
|
+
*/
|
|
14
|
+
export class RNDataChannelAdapter implements DataChannelAdapter {
|
|
15
|
+
bind(channel: RTCDataChannel, handlers: DataChannelHandlers): DataChannelController {
|
|
16
|
+
channel.onopen = () => handlers.onOpen();
|
|
17
|
+
channel.onerror = (e) => handlers.onError(e);
|
|
18
|
+
|
|
19
|
+
const forwardText = (text: string, source: string) => {
|
|
20
|
+
try {
|
|
21
|
+
const parsed = JSON.parse(text) as { event?: string };
|
|
22
|
+
if (parsed?.event) {
|
|
23
|
+
if (parsed.event === 'client_tool_call') {
|
|
24
|
+
console.log(`[RN DataChannel] ${source} client_tool_call decoded`);
|
|
25
|
+
}
|
|
26
|
+
handlers.onMessage({ text });
|
|
27
|
+
} else {
|
|
28
|
+
console.log(`[RN DataChannel] ${source} JSON without event — dropped`);
|
|
29
|
+
}
|
|
30
|
+
} catch (err) {
|
|
31
|
+
console.warn(`[RN DataChannel] ${source} non-JSON frame — dropped`, err);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
channel.onmessage = (e: MessageEvent) => {
|
|
36
|
+
if (typeof e.data === 'string') {
|
|
37
|
+
handlers.onMessage({ text: e.data });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const buffer = toArrayBuffer(e.data);
|
|
42
|
+
if (buffer) {
|
|
43
|
+
try {
|
|
44
|
+
forwardText(decodeUtf8ArrayBuffer(buffer), 'Binary');
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error('[RN DataChannel] Binary decode failed:', err);
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const blob = e.data as Blob | undefined;
|
|
52
|
+
if (blob && typeof blob.text === 'function') {
|
|
53
|
+
blob
|
|
54
|
+
.text()
|
|
55
|
+
.then((text) => forwardText(text, 'Blob'))
|
|
56
|
+
.catch((err) => {
|
|
57
|
+
console.warn('[RN DataChannel] Failed to read Blob message:', err);
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.warn('[RN DataChannel] Unexpected message type:', typeof e.data);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
send(text: string): void {
|
|
67
|
+
if (channel.readyState === 'open') {
|
|
68
|
+
channel.send(text);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
isOpen(): boolean {
|
|
72
|
+
return channel.readyState === 'open';
|
|
73
|
+
},
|
|
74
|
+
close(): void {
|
|
75
|
+
channel.close();
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
import type { MediaAdapter, MediaAudioConstraints } from '../interfaces';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* React Native MediaAdapter.
|
|
6
|
+
*
|
|
7
|
+
* Android: disable heavy DSP (AEC/NS/AGC) — on emulators and some devices they
|
|
8
|
+
* strip speech or pass comfort-noise/beep-like uplink. Shallow constraints
|
|
9
|
+
* match flutter_webrtc VaniraSession on Android.
|
|
10
|
+
*/
|
|
11
|
+
export class RNMediaAdapter implements MediaAdapter {
|
|
12
|
+
async getUserAudio(constraints: MediaAudioConstraints): Promise<MediaStream> {
|
|
13
|
+
const audioConstraints: boolean | MediaTrackConstraints =
|
|
14
|
+
Platform.OS === 'android'
|
|
15
|
+
? {
|
|
16
|
+
echoCancellation: false,
|
|
17
|
+
noiseSuppression: false,
|
|
18
|
+
autoGainControl: false,
|
|
19
|
+
}
|
|
20
|
+
: (constraints as MediaTrackConstraints);
|
|
21
|
+
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
|
+
const stream = (await (navigator as any).mediaDevices.getUserMedia({
|
|
24
|
+
audio: audioConstraints,
|
|
25
|
+
video: false,
|
|
26
|
+
})) as MediaStream;
|
|
27
|
+
|
|
28
|
+
for (const track of stream.getTracks()) {
|
|
29
|
+
if (track.kind === 'audio') {
|
|
30
|
+
track.enabled = true;
|
|
31
|
+
const muted = (track as MediaStreamTrack & { muted?: boolean }).muted;
|
|
32
|
+
console.log(
|
|
33
|
+
`[RNMedia] Local mic track: enabled=${track.enabled}` +
|
|
34
|
+
`${muted !== undefined ? ` muted=${muted}` : ''}` +
|
|
35
|
+
` readyState=${track.readyState} id=${track.id}`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return stream;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
stopStream(stream: MediaStream): void {
|
|
44
|
+
stream.getTracks().forEach((track: MediaStreamTrack) => track.stop());
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { PeerConnectionAdapter, PeerConnectionConfig } from '../PeerConnectionAdapter';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* React Native PeerConnectionAdapter.
|
|
5
|
+
*
|
|
6
|
+
* react-native-webrtc polyfills RTCPeerConnection into the global scope with
|
|
7
|
+
* the same API surface as the browser. This adapter wraps the construction
|
|
8
|
+
* identically to BrowserPeerAdapter — the value is isolation, not difference.
|
|
9
|
+
*
|
|
10
|
+
* Usage note: call `registerGlobals()` from react-native-webrtc once at
|
|
11
|
+
* app entry before constructing this adapter. Without that call,
|
|
12
|
+
* RTCPeerConnection will be undefined at runtime.
|
|
13
|
+
*
|
|
14
|
+
* import { registerGlobals } from 'react-native-webrtc';
|
|
15
|
+
* registerGlobals(); // call once in app entry (e.g. index.js)
|
|
16
|
+
*/
|
|
17
|
+
export class RNPeerAdapter implements PeerConnectionAdapter {
|
|
18
|
+
create(config: PeerConnectionConfig): RTCPeerConnection {
|
|
19
|
+
// react-native-webrtc polyfills RTCPeerConnection as a global.
|
|
20
|
+
// The cast is required because TypeScript's lib.dom.d.ts types differ
|
|
21
|
+
// from the RN polyfill's type declarations for iceServers.
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
|
+
return new (globalThis as any).RTCPeerConnection({
|
|
24
|
+
iceServers: config.iceServers,
|
|
25
|
+
iceTransportPolicy: config.iceTransportPolicy ?? 'all',
|
|
26
|
+
}) as RTCPeerConnection;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import InCallManager from 'react-native-incall-manager';
|
|
2
|
+
|
|
3
|
+
export type CallAudioSnapshot = {
|
|
4
|
+
speakerOn: boolean;
|
|
5
|
+
active: boolean;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
let speakerOn = true;
|
|
9
|
+
let sessionActive = false;
|
|
10
|
+
let cachedSnapshot: CallAudioSnapshot = {speakerOn: true, active: false};
|
|
11
|
+
const listeners = new Set<() => void>();
|
|
12
|
+
|
|
13
|
+
const SERVER_SNAPSHOT: CallAudioSnapshot = {speakerOn: true, active: false};
|
|
14
|
+
|
|
15
|
+
function refreshSnapshot(): CallAudioSnapshot {
|
|
16
|
+
if (
|
|
17
|
+
cachedSnapshot.speakerOn !== speakerOn ||
|
|
18
|
+
cachedSnapshot.active !== sessionActive
|
|
19
|
+
) {
|
|
20
|
+
cachedSnapshot = {speakerOn, active: sessionActive};
|
|
21
|
+
}
|
|
22
|
+
return cachedSnapshot;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Stable snapshot for useSyncExternalStore — must reuse same object when values unchanged. */
|
|
26
|
+
export function getCallAudioSnapshot(): CallAudioSnapshot {
|
|
27
|
+
return refreshSnapshot();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getCallAudioServerSnapshot(): CallAudioSnapshot {
|
|
31
|
+
return SERVER_SNAPSHOT;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function notify(): void {
|
|
35
|
+
refreshSnapshot();
|
|
36
|
+
listeners.forEach(listener => listener());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function subscribeCallAudio(listener: () => void): () => void {
|
|
40
|
+
listeners.add(listener);
|
|
41
|
+
return () => listeners.delete(listener);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isSpeakerOn(): boolean {
|
|
45
|
+
return speakerOn;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isCallAudioSessionActive(): boolean {
|
|
49
|
+
return sessionActive;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Open VoIP audio mode before getUserMedia — earpiece, no ringback.
|
|
54
|
+
* Speaker during capture causes emulator loopback (agent TTS → mic → uplink beep).
|
|
55
|
+
*/
|
|
56
|
+
export function prepareCallAudioForCapture(): void {
|
|
57
|
+
if (sessionActive) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
InCallManager.start({media: 'audio', auto: false, ringback: ''});
|
|
61
|
+
InCallManager.setKeepScreenOn(true);
|
|
62
|
+
InCallManager.setSpeakerphoneOn(false);
|
|
63
|
+
InCallManager.setForceSpeakerphoneOn(false);
|
|
64
|
+
sessionActive = true;
|
|
65
|
+
notify();
|
|
66
|
+
console.log(
|
|
67
|
+
'[CallAudio] Prepared for mic capture (MODE_IN_COMMUNICATION, earpiece, no ringback)',
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Route remote WebRTC audio to speaker/earpiece after connect or first remote track. */
|
|
72
|
+
export function activateCallAudioPlayback(): void {
|
|
73
|
+
if (!sessionActive) {
|
|
74
|
+
prepareCallAudioForCapture();
|
|
75
|
+
}
|
|
76
|
+
applySpeakerRoute(speakerOn);
|
|
77
|
+
notify();
|
|
78
|
+
console.log(`[CallAudio] Playback routing active (speaker=${speakerOn})`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Full session — capture prep + speaker playback (backward compatible). */
|
|
82
|
+
export function startCallAudioSession(): void {
|
|
83
|
+
prepareCallAudioForCapture();
|
|
84
|
+
activateCallAudioPlayback();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Tear down in-call audio session — call on disconnect. */
|
|
88
|
+
export function stopCallAudioSession(): void {
|
|
89
|
+
if (!sessionActive) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
InCallManager.setKeepScreenOn(false);
|
|
93
|
+
InCallManager.stop();
|
|
94
|
+
sessionActive = false;
|
|
95
|
+
notify();
|
|
96
|
+
console.log('[CallAudio] In-call session stopped');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function setSpeakerphoneOn(enabled: boolean): void {
|
|
100
|
+
speakerOn = enabled;
|
|
101
|
+
if (sessionActive) {
|
|
102
|
+
applySpeakerRoute(enabled);
|
|
103
|
+
}
|
|
104
|
+
notify();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function toggleSpeakerphone(): boolean {
|
|
108
|
+
setSpeakerphoneOn(!speakerOn);
|
|
109
|
+
return speakerOn;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function applySpeakerRoute(enabled: boolean): void {
|
|
113
|
+
InCallManager.setSpeakerphoneOn(enabled);
|
|
114
|
+
InCallManager.setForceSpeakerphoneOn(enabled);
|
|
115
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decode UTF-8 bytes from a DataChannel ArrayBuffer.
|
|
3
|
+
* React Native / Hermes may not provide global TextDecoder.
|
|
4
|
+
*/
|
|
5
|
+
export function decodeUtf8ArrayBuffer(buffer: ArrayBuffer): string {
|
|
6
|
+
const Decoder = globalThis.TextDecoder;
|
|
7
|
+
if (typeof Decoder === 'function') {
|
|
8
|
+
return new Decoder('utf-8').decode(buffer);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const bytes = new Uint8Array(buffer);
|
|
12
|
+
let out = '';
|
|
13
|
+
let i = 0;
|
|
14
|
+
|
|
15
|
+
while (i < bytes.length) {
|
|
16
|
+
const byte1 = bytes[i++];
|
|
17
|
+
|
|
18
|
+
if (byte1 < 0x80) {
|
|
19
|
+
out += String.fromCharCode(byte1);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if ((byte1 & 0xe0) === 0xc0) {
|
|
24
|
+
const byte2 = bytes[i++];
|
|
25
|
+
out += String.fromCharCode(((byte1 & 0x1f) << 6) | (byte2 & 0x3f));
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if ((byte1 & 0xf0) === 0xe0) {
|
|
30
|
+
const byte2 = bytes[i++];
|
|
31
|
+
const byte3 = bytes[i++];
|
|
32
|
+
out += String.fromCharCode(
|
|
33
|
+
((byte1 & 0x0f) << 12) | ((byte2 & 0x3f) << 6) | (byte3 & 0x3f),
|
|
34
|
+
);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if ((byte1 & 0xf8) === 0xf0) {
|
|
39
|
+
const byte2 = bytes[i++];
|
|
40
|
+
const byte3 = bytes[i++];
|
|
41
|
+
const byte4 = bytes[i++];
|
|
42
|
+
let codePoint =
|
|
43
|
+
((byte1 & 0x07) << 18) |
|
|
44
|
+
((byte2 & 0x3f) << 12) |
|
|
45
|
+
((byte3 & 0x3f) << 6) |
|
|
46
|
+
(byte4 & 0x3f);
|
|
47
|
+
codePoint -= 0x10000;
|
|
48
|
+
out += String.fromCharCode(
|
|
49
|
+
0xd800 + (codePoint >> 10),
|
|
50
|
+
0xdc00 + (codePoint & 0x3ff),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Normalize react-native-webrtc message payloads to ArrayBuffer. */
|
|
59
|
+
export function toArrayBuffer(data: unknown): ArrayBuffer | null {
|
|
60
|
+
if (data instanceof ArrayBuffer) {
|
|
61
|
+
return data;
|
|
62
|
+
}
|
|
63
|
+
if (ArrayBuffer.isView(data)) {
|
|
64
|
+
const view = data as ArrayBufferView;
|
|
65
|
+
const slice = view.buffer.slice(
|
|
66
|
+
view.byteOffset,
|
|
67
|
+
view.byteOffset + view.byteLength,
|
|
68
|
+
);
|
|
69
|
+
return slice as ArrayBuffer;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|