@opengeni/react-native 1.4.5-canary.37615713321001
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/LICENSE +190 -0
- package/README.md +74 -0
- package/dist/adapters.d.ts +53 -0
- package/dist/adapters.js +3 -0
- package/dist/adapters.js.map +1 -0
- package/dist/attachments.d.ts +75 -0
- package/dist/chunk-3VSZAQ7G.js +391 -0
- package/dist/chunk-3VSZAQ7G.js.map +1 -0
- package/dist/chunk-5ZVK7WKA.js +88 -0
- package/dist/chunk-5ZVK7WKA.js.map +1 -0
- package/dist/chunk-EEDIMY3L.js +240 -0
- package/dist/chunk-EEDIMY3L.js.map +1 -0
- package/dist/chunk-M23SDI4R.js +83 -0
- package/dist/chunk-M23SDI4R.js.map +1 -0
- package/dist/chunk-PHJBSAR3.js +2006 -0
- package/dist/chunk-PHJBSAR3.js.map +1 -0
- package/dist/environment.d.ts +19 -0
- package/dist/expo.d.ts +25 -0
- package/dist/expo.js +267 -0
- package/dist/expo.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +3345 -0
- package/dist/index.js.map +1 -0
- package/dist/presentation.d.ts +262 -0
- package/dist/read-acknowledgement.d.ts +39 -0
- package/dist/realtime/call-events.d.ts +11 -0
- package/dist/realtime/call-view.d.ts +26 -0
- package/dist/realtime/expo-call.d.ts +11 -0
- package/dist/realtime/native-call.d.ts +104 -0
- package/dist/realtime/native-realtime.d.ts +35 -0
- package/dist/realtime/use-native-session-realtime.d.ts +25 -0
- package/dist/session-view.d.ts +87 -0
- package/dist/timeline/activity.d.ts +34 -0
- package/dist/timeline/composer.d.ts +86 -0
- package/dist/timeline/controls.d.ts +27 -0
- package/dist/timeline/decisions.d.ts +29 -0
- package/dist/timeline/feedback.d.ts +26 -0
- package/dist/timeline/icon-nodes.generated.d.ts +738 -0
- package/dist/timeline/icon.d.ts +11 -0
- package/dist/timeline/index.d.ts +32 -0
- package/dist/timeline/index.js +5419 -0
- package/dist/timeline/index.js.map +1 -0
- package/dist/timeline/markdown.d.ts +11 -0
- package/dist/timeline/markdown.js +415 -0
- package/dist/timeline/markdown.js.map +1 -0
- package/dist/timeline/message-attachments.d.ts +13 -0
- package/dist/timeline/message-timeline.d.ts +36 -0
- package/dist/timeline/model-picker.d.ts +17 -0
- package/dist/timeline/primitives.d.ts +85 -0
- package/dist/timeline/queue-dock.d.ts +19 -0
- package/dist/timeline/session-actions.d.ts +34 -0
- package/dist/timeline/session-list.d.ts +50 -0
- package/dist/timeline/session-screen.d.ts +35 -0
- package/dist/timeline/session-signals.d.ts +37 -0
- package/dist/timeline/sheet.d.ts +14 -0
- package/dist/timeline/theme.d.ts +58 -0
- package/dist/timeline/thinking-orb.d.ts +13 -0
- package/dist/timeline/turn.d.ts +80 -0
- package/dist/ui/attention.d.ts +14 -0
- package/dist/ui/cards.d.ts +21 -0
- package/dist/ui/composer.d.ts +21 -0
- package/dist/ui/format.d.ts +13 -0
- package/dist/ui/icons.d.ts +9 -0
- package/dist/ui/index.d.ts +12 -0
- package/dist/ui/index.js +1757 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/steps-sheet.d.ts +10 -0
- package/dist/ui/theme.d.ts +63 -0
- package/dist/ui/timeline.d.ts +48 -0
- package/dist/ui/web-tokens.gen.d.ts +90 -0
- package/dist/use-native-session.d.ts +40 -0
- package/dist/view-model.d.ts +52 -0
- package/dist/voice-input.d.ts +102 -0
- package/dist/webrtc.d.ts +9 -0
- package/dist/webrtc.js +19 -0
- package/dist/webrtc.js.map +1 -0
- package/expo-module.config.json +7 -0
- package/ios/OpenGeniCall.podspec +22 -0
- package/ios/OpenGeniCallAppDelegateSubscriber.swift +29 -0
- package/ios/OpenGeniCallCenter.swift +189 -0
- package/ios/OpenGeniCallModule.swift +64 -0
- package/package.json +93 -0
- package/src/adapters.ts +150 -0
- package/src/attachments.ts +396 -0
- package/src/environment.tsx +214 -0
- package/src/expo.ts +295 -0
- package/src/index.ts +98 -0
- package/src/presentation.ts +285 -0
- package/src/read-acknowledgement.ts +72 -0
- package/src/realtime/call-events.ts +41 -0
- package/src/realtime/call-view.tsx +220 -0
- package/src/realtime/expo-call.ts +49 -0
- package/src/realtime/native-call.ts +266 -0
- package/src/realtime/native-realtime.ts +115 -0
- package/src/realtime/use-native-session-realtime.ts +29 -0
- package/src/session-view.tsx +2251 -0
- package/src/timeline/activity.tsx +736 -0
- package/src/timeline/composer.tsx +686 -0
- package/src/timeline/controls.tsx +151 -0
- package/src/timeline/decisions.tsx +651 -0
- package/src/timeline/feedback.tsx +251 -0
- package/src/timeline/icon-nodes.generated.ts +1830 -0
- package/src/timeline/icon.tsx +44 -0
- package/src/timeline/index.ts +67 -0
- package/src/timeline/markdown.tsx +510 -0
- package/src/timeline/message-attachments.tsx +155 -0
- package/src/timeline/message-timeline.tsx +1042 -0
- package/src/timeline/model-picker.tsx +450 -0
- package/src/timeline/primitives.tsx +561 -0
- package/src/timeline/queue-dock.tsx +331 -0
- package/src/timeline/session-actions.tsx +379 -0
- package/src/timeline/session-list.tsx +338 -0
- package/src/timeline/session-screen.tsx +488 -0
- package/src/timeline/session-signals.tsx +312 -0
- package/src/timeline/sheet.tsx +153 -0
- package/src/timeline/theme.tsx +128 -0
- package/src/timeline/thinking-orb.tsx +93 -0
- package/src/timeline/turn.tsx +569 -0
- package/src/ui/attention.tsx +220 -0
- package/src/ui/cards.tsx +267 -0
- package/src/ui/composer.tsx +296 -0
- package/src/ui/format.ts +86 -0
- package/src/ui/icons.tsx +131 -0
- package/src/ui/index.ts +18 -0
- package/src/ui/steps-sheet.tsx +71 -0
- package/src/ui/theme.ts +261 -0
- package/src/ui/timeline.tsx +837 -0
- package/src/ui/web-tokens.gen.ts +94 -0
- package/src/use-native-session.ts +219 -0
- package/src/view-model.ts +485 -0
- package/src/voice-input.ts +382 -0
- package/src/webrtc.ts +30 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import type { SessionRealtimeControllerSnapshot } from "@opengeni/sdk/realtime";
|
|
3
|
+
import { sessionRealtimeTransportKind } from "@opengeni/sdk/realtime";
|
|
4
|
+
import type { SessionRealtimeModel } from "@opengeni/sdk";
|
|
5
|
+
|
|
6
|
+
/** System events from the platform call service (CallKit on iOS). */
|
|
7
|
+
export type NativeCallEvent =
|
|
8
|
+
/** The person ended the call from the system UI, lock screen or headset. */
|
|
9
|
+
| { type: "ended"; callId: string }
|
|
10
|
+
/** The person muted or unmuted from the system UI. */
|
|
11
|
+
| { type: "muteChanged"; callId: string; muted: boolean }
|
|
12
|
+
/** The audio route changed (speaker, receiver, AirPods, CarPlay). */
|
|
13
|
+
| { type: "routeChanged"; route: NativeCallAudioRoute }
|
|
14
|
+
/** The system activated or released the call audio session (forwarded to WebRTC). */
|
|
15
|
+
| { type: "audioSessionActivated" }
|
|
16
|
+
| { type: "audioSessionDeactivated" }
|
|
17
|
+
/**
|
|
18
|
+
* The system asked the app to start a call from outside it (Siri, the Phone
|
|
19
|
+
* app's recents, an App Intent or a shortcut). `target` is the host's own
|
|
20
|
+
* call target, for example a session id, or absent for the default.
|
|
21
|
+
*/
|
|
22
|
+
| { type: "startRequested"; target: string | null };
|
|
23
|
+
|
|
24
|
+
export type NativeCallAudioRoute = "receiver" | "speaker" | "bluetooth" | "headphones" | "other";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The platform call service seam. The OpenGeni Expo module implements it with
|
|
28
|
+
* CallKit; hosts on other platforms can implement it or omit it, in which case
|
|
29
|
+
* the realtime session runs without a system call.
|
|
30
|
+
*/
|
|
31
|
+
export interface NativeCallAdapter {
|
|
32
|
+
/** Report an outgoing call to the system and activate the call audio session. */
|
|
33
|
+
startCall(call: { callId: string; title: string; target: string | null }): Promise<void>;
|
|
34
|
+
/** The voice connection is live: the system call shows as connected. */
|
|
35
|
+
reportConnected(callId: string): void;
|
|
36
|
+
/** End the system call. `failed` reports a failure rather than a normal hang-up. */
|
|
37
|
+
endCall(callId: string, reason?: "ended" | "failed"): Promise<void>;
|
|
38
|
+
setMuted(callId: string, muted: boolean): Promise<void>;
|
|
39
|
+
/** Route call audio to the loudspeaker, or back to the default route. */
|
|
40
|
+
setSpeaker(on: boolean): Promise<void>;
|
|
41
|
+
subscribe(listener: (event: NativeCallEvent) => void): () => void;
|
|
42
|
+
/** A start request that launched the app before JavaScript subscribed. */
|
|
43
|
+
takePendingStartRequest(): Promise<{ target: string | null } | null>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Native hosts can stream voice over WebRTC; WebSocket audio transports need a browser. */
|
|
47
|
+
export function nativeRealtimeModelSupported(model: SessionRealtimeModel): boolean {
|
|
48
|
+
const transport = sessionRealtimeTransportKind(model);
|
|
49
|
+
return transport === "codex" || transport === "azure-live";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The slice of `useSessionRealtime` a call drives. */
|
|
53
|
+
export type NativeCallRealtime = {
|
|
54
|
+
snapshot: SessionRealtimeControllerSnapshot;
|
|
55
|
+
canStart: boolean;
|
|
56
|
+
admissionBlocker: string | null;
|
|
57
|
+
start(): Promise<void>;
|
|
58
|
+
stop(): Promise<void>;
|
|
59
|
+
setInputMuted(muted: boolean): void;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type NativeCallPhase = "idle" | "connecting" | "active" | "reconnecting" | "ending";
|
|
63
|
+
|
|
64
|
+
export type NativeRealtimeCall = {
|
|
65
|
+
phase: NativeCallPhase;
|
|
66
|
+
muted: boolean;
|
|
67
|
+
speaker: boolean;
|
|
68
|
+
route: NativeCallAudioRoute | null;
|
|
69
|
+
error: string | null;
|
|
70
|
+
/**
|
|
71
|
+
* Whether the system shows this call (CallKit). False when the platform has
|
|
72
|
+
* no call service or refused the call; voice then runs as an in-app call.
|
|
73
|
+
*/
|
|
74
|
+
systemCall: boolean;
|
|
75
|
+
canStart: boolean;
|
|
76
|
+
start(): Promise<void>;
|
|
77
|
+
end(): Promise<void>;
|
|
78
|
+
setMuted(muted: boolean): void;
|
|
79
|
+
setSpeaker(on: boolean): void;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
function randomCallId(): string {
|
|
83
|
+
const crypto = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;
|
|
84
|
+
if (crypto?.randomUUID) return crypto.randomUUID();
|
|
85
|
+
// RFC 4122 v4 layout; CallKit only needs a unique UUID string.
|
|
86
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (char) => {
|
|
87
|
+
const value = Math.floor(Math.random() * 16);
|
|
88
|
+
return (char === "x" ? value : (value & 0x3) | 0x8).toString(16);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Keep one system call and one shared realtime session in lockstep: starting
|
|
94
|
+
* reports an outgoing call and starts voice; the call shows connected once
|
|
95
|
+
* voice is live; hanging up anywhere (app, lock screen, headset) stops voice,
|
|
96
|
+
* and voice ending or failing ends the call. Mute is mirrored both ways.
|
|
97
|
+
* If the system refuses the call (no call service, a region without CallKit,
|
|
98
|
+
* the simulator), voice still starts as an in-app call.
|
|
99
|
+
*/
|
|
100
|
+
export function useNativeRealtimeCall(options: {
|
|
101
|
+
realtime: NativeCallRealtime;
|
|
102
|
+
call?: NativeCallAdapter | null | undefined;
|
|
103
|
+
title: string;
|
|
104
|
+
target?: string | null | undefined;
|
|
105
|
+
}): NativeRealtimeCall {
|
|
106
|
+
const { realtime, call } = options;
|
|
107
|
+
const [callId, setCallId] = useState<string | null>(null);
|
|
108
|
+
const [ending, setEnding] = useState(false);
|
|
109
|
+
const [speaker, setSpeakerState] = useState(false);
|
|
110
|
+
const [route, setRoute] = useState<NativeCallAudioRoute | null>(null);
|
|
111
|
+
const [error, setError] = useState<string | null>(null);
|
|
112
|
+
const [systemCall, setSystemCall] = useState(false);
|
|
113
|
+
const callIdRef = useRef<string | null>(null);
|
|
114
|
+
/** The call id the system knows about; null for an in-app call. */
|
|
115
|
+
const systemCallRef = useRef<string | null>(null);
|
|
116
|
+
const connectedRef = useRef<string | null>(null);
|
|
117
|
+
const realtimeRef = useRef(realtime);
|
|
118
|
+
realtimeRef.current = realtime;
|
|
119
|
+
const status = realtime.snapshot.status;
|
|
120
|
+
const muted = realtime.snapshot.inputMuted;
|
|
121
|
+
|
|
122
|
+
const finish = useCallback(
|
|
123
|
+
async (reason: "ended" | "failed") => {
|
|
124
|
+
const id = callIdRef.current;
|
|
125
|
+
if (!id) return;
|
|
126
|
+
const reported = systemCallRef.current === id;
|
|
127
|
+
callIdRef.current = null;
|
|
128
|
+
systemCallRef.current = null;
|
|
129
|
+
connectedRef.current = null;
|
|
130
|
+
setCallId(null);
|
|
131
|
+
setEnding(false);
|
|
132
|
+
setSpeakerState(false);
|
|
133
|
+
setSystemCall(false);
|
|
134
|
+
if (reported) await call?.endCall(id, reason).catch(() => undefined);
|
|
135
|
+
},
|
|
136
|
+
[call],
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (!call) return;
|
|
141
|
+
return call.subscribe((event) => {
|
|
142
|
+
if (event.type === "routeChanged") {
|
|
143
|
+
setRoute(event.route);
|
|
144
|
+
setSpeakerState(event.route === "speaker");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (
|
|
148
|
+
event.type === "startRequested" ||
|
|
149
|
+
event.type === "audioSessionActivated" ||
|
|
150
|
+
event.type === "audioSessionDeactivated"
|
|
151
|
+
)
|
|
152
|
+
return;
|
|
153
|
+
if (event.callId !== callIdRef.current) return;
|
|
154
|
+
if (event.type === "ended") {
|
|
155
|
+
callIdRef.current = null;
|
|
156
|
+
systemCallRef.current = null;
|
|
157
|
+
connectedRef.current = null;
|
|
158
|
+
setCallId(null);
|
|
159
|
+
setEnding(false);
|
|
160
|
+
setSystemCall(false);
|
|
161
|
+
void realtimeRef.current.stop().catch(() => undefined);
|
|
162
|
+
} else if (event.type === "muteChanged") {
|
|
163
|
+
realtimeRef.current.setInputMuted(event.muted);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}, [call]);
|
|
167
|
+
|
|
168
|
+
// The system call shows connected exactly once, when voice first goes live.
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
const id = callIdRef.current;
|
|
171
|
+
if (id && status === "active" && connectedRef.current !== id) {
|
|
172
|
+
connectedRef.current = id;
|
|
173
|
+
if (systemCallRef.current === id) call?.reportConnected(id);
|
|
174
|
+
}
|
|
175
|
+
}, [call, status, callId]);
|
|
176
|
+
|
|
177
|
+
// Voice ending or failing ends the call; a reconnect keeps it.
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
if (!callIdRef.current) return;
|
|
180
|
+
if (status === "error" || status === "lost_owner") {
|
|
181
|
+
setError(realtime.snapshot.error ?? "Voice disconnected.");
|
|
182
|
+
void finish("failed");
|
|
183
|
+
} else if (status === "idle" && connectedRef.current === callIdRef.current) {
|
|
184
|
+
void finish("ended");
|
|
185
|
+
}
|
|
186
|
+
}, [finish, realtime.snapshot.error, status]);
|
|
187
|
+
|
|
188
|
+
const start = useCallback(async () => {
|
|
189
|
+
if (callIdRef.current) return;
|
|
190
|
+
const current = realtimeRef.current;
|
|
191
|
+
if (!current.canStart) {
|
|
192
|
+
throw new Error(current.admissionBlocker ?? "Voice is not ready yet.");
|
|
193
|
+
}
|
|
194
|
+
const id = randomCallId();
|
|
195
|
+
callIdRef.current = id;
|
|
196
|
+
setCallId(id);
|
|
197
|
+
setError(null);
|
|
198
|
+
if (call) {
|
|
199
|
+
try {
|
|
200
|
+
await call.startCall({ callId: id, title: options.title, target: options.target ?? null });
|
|
201
|
+
if (callIdRef.current === id) {
|
|
202
|
+
systemCallRef.current = id;
|
|
203
|
+
setSystemCall(true);
|
|
204
|
+
}
|
|
205
|
+
} catch {
|
|
206
|
+
// The system refused the call; talk anyway, without the system call UI.
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (callIdRef.current !== id) return;
|
|
210
|
+
try {
|
|
211
|
+
await current.start();
|
|
212
|
+
} catch (cause) {
|
|
213
|
+
setError(cause instanceof Error ? cause.message : "Could not start the call.");
|
|
214
|
+
await finish("failed");
|
|
215
|
+
throw cause;
|
|
216
|
+
}
|
|
217
|
+
}, [call, finish, options.target, options.title]);
|
|
218
|
+
|
|
219
|
+
const end = useCallback(async () => {
|
|
220
|
+
if (!callIdRef.current) return;
|
|
221
|
+
setEnding(true);
|
|
222
|
+
await realtimeRef.current.stop().catch(() => undefined);
|
|
223
|
+
await finish("ended");
|
|
224
|
+
}, [finish]);
|
|
225
|
+
|
|
226
|
+
const setMuted = useCallback(
|
|
227
|
+
(next: boolean) => {
|
|
228
|
+
realtimeRef.current.setInputMuted(next);
|
|
229
|
+
const id = callIdRef.current;
|
|
230
|
+
if (id && systemCallRef.current === id) void call?.setMuted(id, next).catch(() => undefined);
|
|
231
|
+
},
|
|
232
|
+
[call],
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const setSpeaker = useCallback(
|
|
236
|
+
(on: boolean) => {
|
|
237
|
+
setSpeakerState(on);
|
|
238
|
+
void call?.setSpeaker(on).catch(() => setSpeakerState(!on));
|
|
239
|
+
},
|
|
240
|
+
[call],
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
const phase: NativeCallPhase = !callId
|
|
244
|
+
? "idle"
|
|
245
|
+
: ending || status === "stopping"
|
|
246
|
+
? "ending"
|
|
247
|
+
: status === "active"
|
|
248
|
+
? "active"
|
|
249
|
+
: status === "recovering" && connectedRef.current === callId
|
|
250
|
+
? "reconnecting"
|
|
251
|
+
: "connecting";
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
phase,
|
|
255
|
+
muted,
|
|
256
|
+
speaker,
|
|
257
|
+
route,
|
|
258
|
+
error,
|
|
259
|
+
systemCall,
|
|
260
|
+
canStart: !callId && realtime.canStart,
|
|
261
|
+
start,
|
|
262
|
+
end,
|
|
263
|
+
setMuted,
|
|
264
|
+
setSpeaker,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createSessionRealtimeController,
|
|
3
|
+
type CreateSessionRealtimeControllerOptions,
|
|
4
|
+
type SessionRealtimeController,
|
|
5
|
+
type SessionRealtimeOwnerStorage,
|
|
6
|
+
} from "@opengeni/sdk/realtime";
|
|
7
|
+
import type { SessionRealtimeControllerFactory } from "@opengeni/react/session-realtime";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The WebRTC entry points a native host supplies, in the shape of
|
|
11
|
+
* `react-native-webrtc`. Everything else (negotiation, the realtime ledger,
|
|
12
|
+
* delegation, recovery and lifecycle) stays in the shared SDK controller that
|
|
13
|
+
* the web uses.
|
|
14
|
+
*/
|
|
15
|
+
export interface NativeWebRtcAdapter {
|
|
16
|
+
createPeerConnection(): RTCPeerConnection;
|
|
17
|
+
getUserMedia(constraints: MediaStreamConstraints): Promise<MediaStream>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type NativeRealtimeControllerFactoryOptions = {
|
|
21
|
+
/**
|
|
22
|
+
* Owner-proof storage. Defaults to process memory, which matches the web's
|
|
23
|
+
* tab-scoped session storage: a relaunched app recovers through the server
|
|
24
|
+
* lifecycle rather than a stale local proof.
|
|
25
|
+
*/
|
|
26
|
+
storage?: SessionRealtimeOwnerStorage | undefined;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** Process-lifetime owner storage, the native counterpart of `sessionStorage`. */
|
|
30
|
+
export function createMemoryRealtimeOwnerStorage(): SessionRealtimeOwnerStorage {
|
|
31
|
+
const values = new Map<string, string>();
|
|
32
|
+
return {
|
|
33
|
+
getItem: (key) => values.get(key) ?? null,
|
|
34
|
+
setItem: (key, value) => void values.set(key, String(value)),
|
|
35
|
+
removeItem: (key) => void values.delete(key),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type AudioTrackLike = { kind?: string; enabled: boolean };
|
|
40
|
+
type StreamLike = { getAudioTracks?: () => AudioTrackLike[]; getTracks?: () => AudioTrackLike[] };
|
|
41
|
+
|
|
42
|
+
function audioTracks(stream: unknown): AudioTrackLike[] {
|
|
43
|
+
const candidate = stream as StreamLike | null | undefined;
|
|
44
|
+
if (!candidate) return [];
|
|
45
|
+
if (typeof candidate.getAudioTracks === "function") return candidate.getAudioTracks();
|
|
46
|
+
if (typeof candidate.getTracks === "function")
|
|
47
|
+
return candidate.getTracks().filter((track) => track.kind === "audio");
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Native WebRTC plays remote audio itself, through the call audio session. The
|
|
53
|
+
* SDK still drives an audio element (attach, mute, play, detach), so this
|
|
54
|
+
* stand-in maps those calls onto the remote audio tracks: muting disables the
|
|
55
|
+
* tracks, and play resolves because playback is already live.
|
|
56
|
+
*/
|
|
57
|
+
export function createNativeRemoteAudio(): HTMLAudioElement {
|
|
58
|
+
let source: unknown = null;
|
|
59
|
+
let muted = false;
|
|
60
|
+
let paused = true;
|
|
61
|
+
const apply = () => {
|
|
62
|
+
const enabled = !muted && !paused;
|
|
63
|
+
for (const track of audioTracks(source)) track.enabled = enabled;
|
|
64
|
+
};
|
|
65
|
+
const element = {
|
|
66
|
+
autoplay: true,
|
|
67
|
+
get muted() {
|
|
68
|
+
return muted;
|
|
69
|
+
},
|
|
70
|
+
set muted(value: boolean) {
|
|
71
|
+
muted = Boolean(value);
|
|
72
|
+
apply();
|
|
73
|
+
},
|
|
74
|
+
get srcObject() {
|
|
75
|
+
return source;
|
|
76
|
+
},
|
|
77
|
+
set srcObject(value: unknown) {
|
|
78
|
+
source = value ?? null;
|
|
79
|
+
if (source === null) paused = true;
|
|
80
|
+
apply();
|
|
81
|
+
},
|
|
82
|
+
get paused() {
|
|
83
|
+
return paused;
|
|
84
|
+
},
|
|
85
|
+
async play() {
|
|
86
|
+
paused = false;
|
|
87
|
+
apply();
|
|
88
|
+
},
|
|
89
|
+
pause() {
|
|
90
|
+
paused = true;
|
|
91
|
+
apply();
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
return element as unknown as HTMLAudioElement;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Controller factory for `useSessionRealtime({ controllerFactory })`: the
|
|
99
|
+
* shared SDK controller with native WebRTC, a native remote-audio stand-in and
|
|
100
|
+
* owner storage injected. No realtime logic is forked.
|
|
101
|
+
*/
|
|
102
|
+
export function createNativeRealtimeControllerFactory(
|
|
103
|
+
webrtc: NativeWebRtcAdapter,
|
|
104
|
+
options: NativeRealtimeControllerFactoryOptions = {},
|
|
105
|
+
): SessionRealtimeControllerFactory {
|
|
106
|
+
const storage = options.storage ?? createMemoryRealtimeOwnerStorage();
|
|
107
|
+
return (controllerOptions: CreateSessionRealtimeControllerOptions): SessionRealtimeController =>
|
|
108
|
+
createSessionRealtimeController({
|
|
109
|
+
...controllerOptions,
|
|
110
|
+
storage: controllerOptions.storage ?? storage,
|
|
111
|
+
remoteAudio: controllerOptions.remoteAudio ?? createNativeRemoteAudio(),
|
|
112
|
+
createPeerConnection: () => webrtc.createPeerConnection(),
|
|
113
|
+
getUserMedia: (constraints) => webrtc.getUserMedia(constraints),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { useSessionRealtime } from "@opengeni/react/session-realtime";
|
|
3
|
+
import {
|
|
4
|
+
createNativeRealtimeControllerFactory,
|
|
5
|
+
type NativeRealtimeControllerFactoryOptions,
|
|
6
|
+
type NativeWebRtcAdapter,
|
|
7
|
+
} from "./native-realtime";
|
|
8
|
+
|
|
9
|
+
export type UseNativeSessionRealtimeOptions = Omit<
|
|
10
|
+
Parameters<typeof useSessionRealtime>[0],
|
|
11
|
+
"controllerFactory"
|
|
12
|
+
> & {
|
|
13
|
+
/** Native WebRTC, for example `createReactNativeWebRtcAdapter()` from `./webrtc`. */
|
|
14
|
+
webrtc: NativeWebRtcAdapter;
|
|
15
|
+
storage?: NativeRealtimeControllerFactoryOptions["storage"];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The web's `useSessionRealtime`, driven by native WebRTC. Same controller,
|
|
20
|
+
* same admission rules, same lifecycle recovery; only media is injected.
|
|
21
|
+
*/
|
|
22
|
+
export function useNativeSessionRealtime(options: UseNativeSessionRealtimeOptions) {
|
|
23
|
+
const { webrtc, storage, ...realtimeOptions } = options;
|
|
24
|
+
const controllerFactory = useMemo(
|
|
25
|
+
() => createNativeRealtimeControllerFactory(webrtc, storage ? { storage } : {}),
|
|
26
|
+
[webrtc, storage],
|
|
27
|
+
);
|
|
28
|
+
return useSessionRealtime({ ...realtimeOptions, controllerFactory });
|
|
29
|
+
}
|