@marianmeres/webrtc 1.2.5 → 1.3.0

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/AGENTS.md CHANGED
@@ -159,6 +159,15 @@ interface WebRtcManagerConfig {
159
159
  }
160
160
  ```
161
161
 
162
+ ### GatherIceCandidatesOptions Interface
163
+
164
+ ```typescript
165
+ interface GatherIceCandidatesOptions {
166
+ timeout?: number; // Timeout in ms (default: 10000)
167
+ onCandidate?: (candidate: RTCIceCandidate | null) => void; // Called for each candidate
168
+ }
169
+ ```
170
+
162
171
  ### Properties (Getters)
163
172
 
164
173
  | Property | Type | Description |
@@ -205,6 +214,7 @@ interface WebRtcManagerConfig {
205
214
  | setRemoteDescription | `(description: RTCSessionDescriptionInit): Promise<boolean>` | Set remote SDP |
206
215
  | addIceCandidate | `(candidate: RTCIceCandidateInit \| null): Promise<boolean>` | Add ICE candidate |
207
216
  | iceRestart | `(): Promise<boolean>` | Perform ICE restart |
217
+ | gatherIceCandidates | `(options?: GatherIceCandidatesOptions): Promise<void>` | Wait for ICE gathering to complete |
208
218
  | getLocalDescription | `(): RTCSessionDescription \| null` | Get local SDP |
209
219
  | getRemoteDescription | `(): RTCSessionDescription \| null` | Get remote SDP |
210
220
  | getStats | `(): Promise<RTCStatsReport \| null>` | Get connection statistics |
package/README.md CHANGED
@@ -95,6 +95,35 @@ await manager.setLocalDescription(offer)
95
95
  await manager.setRemoteDescription(answer)
96
96
  await manager.addIceCandidate(candidate)
97
97
  await manager.iceRestart() // Trigger ICE restart
98
+ await manager.gatherIceCandidates(options) // Wait for ICE gathering to complete
99
+ ```
100
+
101
+ ### gatherIceCandidates(options?)
102
+
103
+ Wait for ICE gathering to complete. Useful for HTTP POST signaling patterns where you need all ICE candidates bundled in the local description before sending to the server.
104
+
105
+ **Options:** `timeout` (ms, default 10000), `onCandidate` (callback for each candidate)
106
+
107
+ ```typescript
108
+ const offer = await manager.createOffer();
109
+ await manager.setLocalDescription(offer);
110
+ await manager.gatherIceCandidates({ timeout: 5000 });
111
+ // Now manager.peerConnection.localDescription has all ICE candidates bundled
112
+ ```
113
+
114
+ **Error Handling:** A timeout rejection does *not* transition the FSM to ERROR state. This is intentional - `gatherIceCandidates()` is a utility method, and a timeout means "gathering didn't complete in time", not "the connection failed". The consumer decides how to handle it:
115
+
116
+ ```typescript
117
+ try {
118
+ await manager.gatherIceCandidates({ timeout: 5000 });
119
+ } catch (e) {
120
+ if (e.message === "ICE gathering timeout") {
121
+ // Options:
122
+ // 1. Retry with longer timeout
123
+ // 2. Proceed anyway - localDescription may have partial candidates
124
+ // 3. Treat as fatal: manager.reset()
125
+ }
126
+ }
98
127
  ```
99
128
 
100
129
  ### Data Channel Methods
@@ -105,6 +134,54 @@ const dc = manager.getDataChannel(label)
105
134
  manager.sendData(label, data) // Returns boolean
106
135
  ```
107
136
 
137
+ ### Working with External Audio Streams
138
+
139
+ You might want to keep `enableMicrophone: false` even when your application uses audio. Common scenarios include:
140
+
141
+ - **Pre-acquired stream**: You may have obtained the audio stream earlier in the application flow (e.g., during a permissions check, in a lobby/waiting room, or for local audio preview before joining)
142
+ - **Custom audio processing**: You want to apply audio effects, noise suppression, or other processing via Web Audio API before transmitting
143
+ - **Multiple sources**: You need to mix audio from multiple sources (microphone + system audio, multiple microphones, background music, etc.)
144
+ - **Fine-grained privacy control**: You want explicit control over exactly when the microphone activates
145
+ - **Testing**: You want to inject synthetic audio (e.g., oscillator tones) for automated testing
146
+
147
+ To use your own audio stream, access the `peerConnection` directly and add tracks after initialization:
148
+
149
+ ```typescript
150
+ const manager = new WebRtcManager(factory, {
151
+ peerConfig: { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] },
152
+ enableMicrophone: false, // We'll handle the audio stream ourselves
153
+ });
154
+
155
+ // Your pre-acquired or processed audio stream
156
+ const myAudioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
157
+
158
+ // Or a processed stream via Web Audio API
159
+ const audioCtx = new AudioContext();
160
+ const source = audioCtx.createMediaStreamSource(myAudioStream);
161
+ const gainNode = audioCtx.createGain();
162
+ gainNode.gain.value = 0.8;
163
+ source.connect(gainNode);
164
+ const destination = audioCtx.createMediaStreamDestination();
165
+ gainNode.connect(destination);
166
+ const processedStream = destination.stream;
167
+
168
+ // Initialize the manager
169
+ await manager.initialize();
170
+
171
+ // Add your audio track to the peer connection
172
+ const pc = manager.peerConnection;
173
+ if (pc) {
174
+ processedStream.getAudioTracks().forEach((track) => {
175
+ pc.addTrack(track, processedStream);
176
+ });
177
+ }
178
+
179
+ // Continue with normal connection flow
180
+ await manager.connect();
181
+ const offer = await manager.createOffer();
182
+ // ...
183
+ ```
184
+
108
185
  ### Event Subscription
109
186
 
110
187
  ```typescript
package/dist/types.d.ts CHANGED
@@ -156,3 +156,13 @@ export interface WebRtcEvents {
156
156
  /** Emitted when an error occurs. Payload: the Error object. */
157
157
  error: Error;
158
158
  }
159
+ /**
160
+ * Options for waiting for ICE gathering to complete.
161
+ * Used with gatherIceCandidates() for HTTP POST signaling patterns.
162
+ */
163
+ export interface GatherIceCandidatesOptions {
164
+ /** Timeout in milliseconds (default: 10000) */
165
+ timeout?: number;
166
+ /** Called for each ICE candidate as it's gathered */
167
+ onCandidate?: (candidate: RTCIceCandidate | null) => void;
168
+ }
@@ -1,4 +1,4 @@
1
- import { type WebRtcFactory, type WebRtcManagerConfig, WebRtcState, type WebRtcEvents } from "./types.js";
1
+ import { type WebRtcFactory, type WebRtcManagerConfig, WebRtcState, type WebRtcEvents, type GatherIceCandidatesOptions } from "./types.js";
2
2
  /**
3
3
  * WebRTC connection manager with FSM-based lifecycle and event-driven architecture.
4
4
  *
@@ -203,6 +203,13 @@ export declare class WebRtcManager<TContext = unknown> {
203
203
  * @returns True if successful, false otherwise.
204
204
  */
205
205
  iceRestart(): Promise<boolean>;
206
+ /**
207
+ * Wait for ICE gathering to complete.
208
+ * Use this for HTTP POST signaling patterns where you need all ICE candidates
209
+ * bundled in the local description before sending to the server.
210
+ * @param options - Optional configuration for timeout and candidate callback.
211
+ */
212
+ gatherIceCandidates(options?: GatherIceCandidatesOptions): Promise<void>;
206
213
  /**
207
214
  * Returns the current local session description.
208
215
  * @returns The local description, or null if not set.
@@ -675,6 +675,52 @@ export class WebRtcManager {
675
675
  return false;
676
676
  }
677
677
  }
678
+ /**
679
+ * Wait for ICE gathering to complete.
680
+ * Use this for HTTP POST signaling patterns where you need all ICE candidates
681
+ * bundled in the local description before sending to the server.
682
+ * @param options - Optional configuration for timeout and candidate callback.
683
+ */
684
+ gatherIceCandidates(options = {}) {
685
+ const { timeout = 10000, onCandidate } = options;
686
+ if (!this.#pc) {
687
+ return Promise.reject(new Error("Peer connection not initialized"));
688
+ }
689
+ const pc = this.#pc;
690
+ if (pc.iceGatheringState === "complete") {
691
+ this.#logDebug("ICE gathering already complete");
692
+ return Promise.resolve();
693
+ }
694
+ this.#logDebug("Waiting for ICE gathering to complete...");
695
+ return new Promise((resolve, reject) => {
696
+ const timer = setTimeout(() => {
697
+ cleanup();
698
+ reject(new Error("ICE gathering timeout"));
699
+ }, timeout);
700
+ const cleanup = () => {
701
+ clearTimeout(timer);
702
+ pc.removeEventListener("icegatheringstatechange", checkState);
703
+ pc.removeEventListener("icecandidate", handleCandidate);
704
+ };
705
+ const checkState = () => {
706
+ if (pc.iceGatheringState === "complete") {
707
+ this.#logDebug("ICE gathering complete (via state change)");
708
+ cleanup();
709
+ resolve();
710
+ }
711
+ };
712
+ const handleCandidate = (event) => {
713
+ onCandidate?.(event.candidate);
714
+ if (event.candidate === null) {
715
+ this.#logDebug("ICE gathering complete (null candidate)");
716
+ cleanup();
717
+ resolve();
718
+ }
719
+ };
720
+ pc.addEventListener("icegatheringstatechange", checkState);
721
+ pc.addEventListener("icecandidate", handleCandidate);
722
+ });
723
+ }
678
724
  /**
679
725
  * Returns the current local session description.
680
726
  * @returns The local description, or null if not set.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/webrtc",
3
- "version": "1.2.5",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "main": "dist/mod.js",
6
6
  "types": "dist/mod.d.ts",