@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.
Files changed (132) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +74 -0
  3. package/dist/adapters.d.ts +53 -0
  4. package/dist/adapters.js +3 -0
  5. package/dist/adapters.js.map +1 -0
  6. package/dist/attachments.d.ts +75 -0
  7. package/dist/chunk-3VSZAQ7G.js +391 -0
  8. package/dist/chunk-3VSZAQ7G.js.map +1 -0
  9. package/dist/chunk-5ZVK7WKA.js +88 -0
  10. package/dist/chunk-5ZVK7WKA.js.map +1 -0
  11. package/dist/chunk-EEDIMY3L.js +240 -0
  12. package/dist/chunk-EEDIMY3L.js.map +1 -0
  13. package/dist/chunk-M23SDI4R.js +83 -0
  14. package/dist/chunk-M23SDI4R.js.map +1 -0
  15. package/dist/chunk-PHJBSAR3.js +2006 -0
  16. package/dist/chunk-PHJBSAR3.js.map +1 -0
  17. package/dist/environment.d.ts +19 -0
  18. package/dist/expo.d.ts +25 -0
  19. package/dist/expo.js +267 -0
  20. package/dist/expo.js.map +1 -0
  21. package/dist/index.d.ts +13 -0
  22. package/dist/index.js +3345 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/presentation.d.ts +262 -0
  25. package/dist/read-acknowledgement.d.ts +39 -0
  26. package/dist/realtime/call-events.d.ts +11 -0
  27. package/dist/realtime/call-view.d.ts +26 -0
  28. package/dist/realtime/expo-call.d.ts +11 -0
  29. package/dist/realtime/native-call.d.ts +104 -0
  30. package/dist/realtime/native-realtime.d.ts +35 -0
  31. package/dist/realtime/use-native-session-realtime.d.ts +25 -0
  32. package/dist/session-view.d.ts +87 -0
  33. package/dist/timeline/activity.d.ts +34 -0
  34. package/dist/timeline/composer.d.ts +86 -0
  35. package/dist/timeline/controls.d.ts +27 -0
  36. package/dist/timeline/decisions.d.ts +29 -0
  37. package/dist/timeline/feedback.d.ts +26 -0
  38. package/dist/timeline/icon-nodes.generated.d.ts +738 -0
  39. package/dist/timeline/icon.d.ts +11 -0
  40. package/dist/timeline/index.d.ts +32 -0
  41. package/dist/timeline/index.js +5419 -0
  42. package/dist/timeline/index.js.map +1 -0
  43. package/dist/timeline/markdown.d.ts +11 -0
  44. package/dist/timeline/markdown.js +415 -0
  45. package/dist/timeline/markdown.js.map +1 -0
  46. package/dist/timeline/message-attachments.d.ts +13 -0
  47. package/dist/timeline/message-timeline.d.ts +36 -0
  48. package/dist/timeline/model-picker.d.ts +17 -0
  49. package/dist/timeline/primitives.d.ts +85 -0
  50. package/dist/timeline/queue-dock.d.ts +19 -0
  51. package/dist/timeline/session-actions.d.ts +34 -0
  52. package/dist/timeline/session-list.d.ts +50 -0
  53. package/dist/timeline/session-screen.d.ts +35 -0
  54. package/dist/timeline/session-signals.d.ts +37 -0
  55. package/dist/timeline/sheet.d.ts +14 -0
  56. package/dist/timeline/theme.d.ts +58 -0
  57. package/dist/timeline/thinking-orb.d.ts +13 -0
  58. package/dist/timeline/turn.d.ts +80 -0
  59. package/dist/ui/attention.d.ts +14 -0
  60. package/dist/ui/cards.d.ts +21 -0
  61. package/dist/ui/composer.d.ts +21 -0
  62. package/dist/ui/format.d.ts +13 -0
  63. package/dist/ui/icons.d.ts +9 -0
  64. package/dist/ui/index.d.ts +12 -0
  65. package/dist/ui/index.js +1757 -0
  66. package/dist/ui/index.js.map +1 -0
  67. package/dist/ui/steps-sheet.d.ts +10 -0
  68. package/dist/ui/theme.d.ts +63 -0
  69. package/dist/ui/timeline.d.ts +48 -0
  70. package/dist/ui/web-tokens.gen.d.ts +90 -0
  71. package/dist/use-native-session.d.ts +40 -0
  72. package/dist/view-model.d.ts +52 -0
  73. package/dist/voice-input.d.ts +102 -0
  74. package/dist/webrtc.d.ts +9 -0
  75. package/dist/webrtc.js +19 -0
  76. package/dist/webrtc.js.map +1 -0
  77. package/expo-module.config.json +7 -0
  78. package/ios/OpenGeniCall.podspec +22 -0
  79. package/ios/OpenGeniCallAppDelegateSubscriber.swift +29 -0
  80. package/ios/OpenGeniCallCenter.swift +189 -0
  81. package/ios/OpenGeniCallModule.swift +64 -0
  82. package/package.json +93 -0
  83. package/src/adapters.ts +150 -0
  84. package/src/attachments.ts +396 -0
  85. package/src/environment.tsx +214 -0
  86. package/src/expo.ts +295 -0
  87. package/src/index.ts +98 -0
  88. package/src/presentation.ts +285 -0
  89. package/src/read-acknowledgement.ts +72 -0
  90. package/src/realtime/call-events.ts +41 -0
  91. package/src/realtime/call-view.tsx +220 -0
  92. package/src/realtime/expo-call.ts +49 -0
  93. package/src/realtime/native-call.ts +266 -0
  94. package/src/realtime/native-realtime.ts +115 -0
  95. package/src/realtime/use-native-session-realtime.ts +29 -0
  96. package/src/session-view.tsx +2251 -0
  97. package/src/timeline/activity.tsx +736 -0
  98. package/src/timeline/composer.tsx +686 -0
  99. package/src/timeline/controls.tsx +151 -0
  100. package/src/timeline/decisions.tsx +651 -0
  101. package/src/timeline/feedback.tsx +251 -0
  102. package/src/timeline/icon-nodes.generated.ts +1830 -0
  103. package/src/timeline/icon.tsx +44 -0
  104. package/src/timeline/index.ts +67 -0
  105. package/src/timeline/markdown.tsx +510 -0
  106. package/src/timeline/message-attachments.tsx +155 -0
  107. package/src/timeline/message-timeline.tsx +1042 -0
  108. package/src/timeline/model-picker.tsx +450 -0
  109. package/src/timeline/primitives.tsx +561 -0
  110. package/src/timeline/queue-dock.tsx +331 -0
  111. package/src/timeline/session-actions.tsx +379 -0
  112. package/src/timeline/session-list.tsx +338 -0
  113. package/src/timeline/session-screen.tsx +488 -0
  114. package/src/timeline/session-signals.tsx +312 -0
  115. package/src/timeline/sheet.tsx +153 -0
  116. package/src/timeline/theme.tsx +128 -0
  117. package/src/timeline/thinking-orb.tsx +93 -0
  118. package/src/timeline/turn.tsx +569 -0
  119. package/src/ui/attention.tsx +220 -0
  120. package/src/ui/cards.tsx +267 -0
  121. package/src/ui/composer.tsx +296 -0
  122. package/src/ui/format.ts +86 -0
  123. package/src/ui/icons.tsx +131 -0
  124. package/src/ui/index.ts +18 -0
  125. package/src/ui/steps-sheet.tsx +71 -0
  126. package/src/ui/theme.ts +261 -0
  127. package/src/ui/timeline.tsx +837 -0
  128. package/src/ui/web-tokens.gen.ts +94 -0
  129. package/src/use-native-session.ts +219 -0
  130. package/src/view-model.ts +485 -0
  131. package/src/voice-input.ts +382 -0
  132. 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
+ }