From f9f8d6da7be29617720b68b714c7f625a48c3f48 Mon Sep 17 00:00:00 2001 From: USAMAWIZARD Date: Wed, 6 May 2026 01:00:57 +0530 Subject: [PATCH 1/4] stop the audio video tracks when disabled to stop sending rtp data to the server --- .project | 28 ++++ .settings/org.eclipse.buildship.core.prefs | 13 ++ CRASH_FIX_SUMMARY.md | 136 ++++++++++++++++++ QUICK_FIX_REFERENCE.md | 98 +++++++++++++ .../api/IWebRTCClient.java | 16 +++ .../api/WebRTCClientConfig.java | 13 ++ .../core/WebRTCClient.java | 83 +++++++++-- .../WebRTCClientTest.java | 40 ++++++ .../basic/ConferenceActivity.java | 3 + 9 files changed, 416 insertions(+), 14 deletions(-) create mode 100644 .project create mode 100644 .settings/org.eclipse.buildship.core.prefs create mode 100644 CRASH_FIX_SUMMARY.md create mode 100644 QUICK_FIX_REFERENCE.md diff --git a/.project b/.project new file mode 100644 index 00000000..e7bd531d --- /dev/null +++ b/.project @@ -0,0 +1,28 @@ + + + WebRTC-Android-SDK + Project WebRTC-Android-SDK created by Buildship. + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.buildship.core.gradleprojectnature + + + + 1737395867684 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + + diff --git a/.settings/org.eclipse.buildship.core.prefs b/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 00000000..e583fcee --- /dev/null +++ b/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,13 @@ +arguments=--init-script /home/usama/.local/share/nvim/mason/packages/jdtls/config_linux/org.eclipse.osgi/58/0/.cp/gradle/init/init.gradle +auto.sync=false +build.scans.enabled=false +connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) +connection.project.dir= +eclipse.preferences.version=1 +gradle.user.home= +java.home=/usr/lib/jvm/java-17-openjdk +jvm.arguments= +offline.mode=false +override.workspace.settings=true +show.console.view=true +show.executions.view=true diff --git a/CRASH_FIX_SUMMARY.md b/CRASH_FIX_SUMMARY.md new file mode 100644 index 00000000..f42ff875 --- /dev/null +++ b/CRASH_FIX_SUMMARY.md @@ -0,0 +1,136 @@ +# WebRTC Multiple Connection Reconnection Race Condition Fix + +## Problem Summary + +The application was experiencing native crashes (SIGSEGV - Signal 11) when multiple WebRTC connections attempted to reconnect simultaneously. The crash occurred in the native `libjingle_peerconnection_so.so` library on the `network_thread`. + +### Crash Details +- **Signal**: SIGSEGV (Segmentation Fault) +- **Location**: Native WebRTC library (`libjingle_peerconnection_so.so`) +- **Thread**: network_thread +- **Trigger**: Multiple WebRTC connections reconnecting at the same time + +### Root Cause Analysis + +The crash was caused by **critical race conditions** in the peer connection management code: + +1. **Unsynchronized Access**: Multiple threads (reconnection handlers, executor threads, network threads) were accessing `PeerInfo.peerConnection` without proper synchronization. + +2. **Dangling References**: After calling `pc.close()`, the `peerConnection` reference wasn't being set to `null`, allowing subsequent code to access closed connections. + +3. **Timing Issues**: The native `network_thread` in WebRTC was attempting to access peer connection objects while they were being closed on another thread. + +4. **Multiple Reconnection Handlers**: Three separate reconnection runnables (`publishReconnectorRunnable`, `playReconnectorRunnable`, `peerReconnectorRunnable`) could run concurrently and attempt to close/recreate the same connections. + +## Solution Implemented + +Added **comprehensive thread synchronization** using the `PeerInfo` object as a lock monitor for all operations that access or modify peer connections. + +### Changes Made + +#### 1. Reconnection Runnables (Lines 266-433) +- Added `synchronized (peerInfo)` blocks around all peer connection access +- Added `peerInfo.peerConnection = null` after closing connections +- Wrapped `pc.close()` in try-catch blocks to handle exceptions gracefully + +**Example:** +```java +synchronized (peerInfo) { + PeerConnection pc = peerInfo.peerConnection; + if (pc != null) { + try { + pc.close(); + } catch (Exception e) { + Log.e(TAG, "Error closing peer connection during reconnection", e); + } + peerInfo.peerConnection = null; + } + // ... reconnection logic +} +``` + +#### 2. Peer Connection Creation (Line 2196-2198) +- Added synchronization when assigning the peer connection reference + +```java +synchronized (peer) { + peer.peerConnection = peerConnection; +} +``` + +#### 3. SDP Observer Methods (Lines 705-759) +- Added synchronization in `onCreateSuccess()` callback +- Protected access to peer connection during SDP operations + +#### 4. WebSocket Connection Handler (Lines 1029-1038) +- Added synchronization when checking if peer connections need to be created + +#### 5. Configuration Handler (Lines 1635-1641) +- Added synchronization when checking for null peer connections in offer handling + +#### 6. ICE Candidate Handling (Lines 2526-2546) +- Added synchronization when adding remote ICE candidates + +#### 7. Cleanup Operations (Lines 2293-2322) +- Added synchronization in `closeInternal()` method +- Protected cleanup of senders, tracks, and data channels +- Added exception handling for cleanup operations + +#### 8. Helper Methods +- **`getPeerConnectionFor()`** (Lines 2886-2891): Added synchronization when accessing peer connection +- **`drainCandidates()`** (Lines 2862-2885): Added synchronization for the entire candidate draining process +- **`initDataChannel()`** (Lines 2262-2270): Added synchronization when creating data channels + +## Technical Details + +### Synchronization Strategy + +- **Lock Object**: Each `PeerInfo` instance is used as its own lock monitor +- **Granularity**: Fine-grained locking at the individual peer level (not global lock) +- **Benefits**: + - Allows concurrent operations on different peers + - Prevents race conditions on the same peer + - Minimal performance impact + +### Why This Fix Works + +1. **Atomicity**: Operations on peer connections are now atomic - close and null assignment happen together +2. **Visibility**: Synchronized blocks ensure memory visibility across threads +3. **Ordering**: Prevents reordering of operations that could lead to accessing closed connections +4. **Native Thread Safety**: Prevents the native `network_thread` from accessing objects being destroyed + +## Testing Recommendations + +1. **Stress Test**: Simulate multiple simultaneous connection drops and reconnections +2. **Conference Mode**: Test with multiple publish and play streams in conference mode +3. **Network Instability**: Test with unstable network conditions to trigger frequent reconnections +4. **Memory Profiling**: Ensure no memory leaks from the synchronization changes +5. **Performance Testing**: Verify that synchronization doesn't introduce performance degradation + +## Files Modified + +- `webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/core/WebRTCClient.java` + +## Build Status + +✅ Build successful - No compilation errors or lint issues + +## Additional Notes + +- The existing comment in the code (lines 289-291) mentions that using `dispose()` instead of `close()` causes segmentation faults. This fix addresses a related but different race condition. +- The fix is backward compatible and doesn't change the public API +- All exception handling has been added to prevent crashes from propagating + +## Prevention + +To prevent similar issues in the future: + +1. Always synchronize access to `peerInfo.peerConnection` +2. Set references to `null` after closing native objects +3. Use try-catch blocks around native operations +4. Consider the threading model when adding new peer connection operations + +## Related Issues + +This fix addresses the intermittent crash that occurred "time to time when multiple webrtc connection tries to reconnect at same time" as reported in the crash logs. + diff --git a/QUICK_FIX_REFERENCE.md b/QUICK_FIX_REFERENCE.md new file mode 100644 index 00000000..d7c9dfcb --- /dev/null +++ b/QUICK_FIX_REFERENCE.md @@ -0,0 +1,98 @@ +# Quick Reference: Race Condition Fix for Multiple WebRTC Reconnections + +## What Was Fixed + +Native crash (SIGSEGV) when multiple WebRTC connections reconnect simultaneously. + +## Key Pattern Applied + +### Before (Unsafe): +```java +PeerConnection pc = peerInfo.peerConnection; +if (pc != null) { + pc.close(); +} +// peerConnection reference still points to closed object! +``` + +### After (Thread-Safe): +```java +synchronized (peerInfo) { + PeerConnection pc = peerInfo.peerConnection; + if (pc != null) { + try { + pc.close(); + } catch (Exception e) { + Log.e(TAG, "Error closing peer connection", e); + } + peerInfo.peerConnection = null; // Clear reference + } +} +``` + +## Critical Changes Summary + +| Location | Change | Why | +|----------|--------|-----| +| Reconnection Runnables | Added `synchronized(peerInfo)` + null assignment | Prevent concurrent close/access | +| `createPeerConnectionInternal()` | Synchronized peer assignment | Prevent assignment during close | +| SDP Callbacks | Synchronized peer access | Prevent access to closing connection | +| `closeInternal()` | Synchronized cleanup loop | Prevent concurrent modification | +| `getPeerConnectionFor()` | Synchronized return | Ensure consistent view | +| `drainCandidates()` | Synchronized entire method | Prevent ICE candidate race | +| `initDataChannel()` | Synchronized channel creation | Prevent creation on closing peer | + +## Rule of Thumb + +**Always synchronize on `peerInfo` when:** +1. Accessing `peerInfo.peerConnection` +2. Modifying `peerInfo.peerConnection` +3. Calling methods on `peerInfo.peerConnection` +4. Closing or disposing peer connections + +## Pattern to Follow + +```java +PeerInfo peerInfo = getPeerInfoFor(streamId); +if (peerInfo != null) { + synchronized (peerInfo) { + PeerConnection pc = peerInfo.peerConnection; + if (pc != null) { + try { + // Do something with pc + pc.someMethod(); + } catch (Exception e) { + Log.e(TAG, "Error", e); + } + } + } +} +``` + +## What NOT to Do + +❌ **Don't**: Access `peerConnection` without synchronization +❌ **Don't**: Keep references to `PeerConnection` outside synchronized blocks +❌ **Don't**: Call `close()` without setting the reference to null +❌ **Don't**: Assume the native object is valid after getting the reference + +## Testing Checklist + +- [ ] Multiple connections reconnecting simultaneously +- [ ] Conference mode with many participants +- [ ] Rapid connect/disconnect cycles +- [ ] Network instability simulation +- [ ] Memory leak check (no dangling references) + +## Build Verification + +```bash +./gradlew :webrtc-android-framework:assembleDebug +``` + +Expected: ✅ BUILD SUCCESSFUL + +## Branch + +`improveReconnection` + diff --git a/webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/api/IWebRTCClient.java b/webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/api/IWebRTCClient.java index 9ac35367..a0a40ac1 100644 --- a/webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/api/IWebRTCClient.java +++ b/webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/api/IWebRTCClient.java @@ -194,6 +194,22 @@ void publish(String streamId, String token, boolean videoCallEnabled, boolean au */ void setAudioEnabled(boolean enabled); + /** + * Disable black frame sender when video is toggled off via toggleSendVideo(false). + * When true, no black frames will be sent when the camera is turned off during a call. + * + * @param disable true to disable black frame sender, false to enable (default) + */ + void setDisableBlackFrameSender(boolean disable); + + /** + * Disable silence packets when audio is muted via toggleSendAudio(false). + * When true, RTP transmission is stopped when muted (no silence packets sent). + * + * @param disable true to stop RTP when muted, false for default behavior (sends silence) + */ + void setDisableSilenceWhenMuted(boolean disable); + /** * enable/disable played track stream from the server * diff --git a/webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/api/WebRTCClientConfig.java b/webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/api/WebRTCClientConfig.java index 531f6abb..cd29d891 100644 --- a/webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/api/WebRTCClientConfig.java +++ b/webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/api/WebRTCClientConfig.java @@ -183,4 +183,17 @@ public class WebRTCClientConfig { * Flag for connecting bluetooth headphones. */ public boolean bluetoothEnabled = false; + + /* + * Flag indicating whether black frame sender is disabled when video is toggled off. + * When true, no black frames will be sent when toggleSendVideo(false) is called. + */ + public boolean disableBlackFrameSender = false; + + /* + * Flag indicating whether silence packets are disabled when audio is muted. + * When true, RTP transmission is stopped when toggleSendAudio(false) is called (no silence packets sent). + * When false, muted audio still sends silence packets (default WebRTC behavior). + */ + public boolean disableSilenceWhenMuted = false; } diff --git a/webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/core/WebRTCClient.java b/webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/core/WebRTCClient.java index 8e6c929f..3263bdbd 100644 --- a/webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/core/WebRTCClient.java +++ b/webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/core/WebRTCClient.java @@ -1853,23 +1853,50 @@ public void sendMessageViaDataChannel(String streamId, DataChannel.Buffer buffer } public void changeVideoCapturer(VideoCapturer newVideoCapturer) { - try { - if (videoCapturer != null) { - videoCapturer.stopCapture(); - } - } catch (InterruptedException e) { - e.printStackTrace(); - } - - videoCapturerStopped = true; + VideoCapturer oldVideoCapturer = videoCapturer; + VideoTrack oldVideoTrack = localVideoTrack; videoCapturer = newVideoCapturer; localVideoTrack = null; MediaStreamTrack newTrack = createVideoTrack(videoCapturer); - if (localVideoSender != null) { - localVideoSender.setTrack(newTrack, true); + RtpSender videoSender = findVideoSender(); + if (videoSender == null || newTrack == null) { + Log.e(TAG, "Video sender or new video track is not available while changing video capturer."); + stopVideoCapturer(newVideoCapturer); + videoCapturer = oldVideoCapturer; + localVideoTrack = oldVideoTrack; + return; + } + + try { + if (!videoSender.setTrack(newTrack, true)) { + Log.e(TAG, "RtpSender.setTrack failed while changing video capturer."); + stopVideoCapturer(newVideoCapturer); + videoCapturer = oldVideoCapturer; + localVideoTrack = oldVideoTrack; + return; + } + } catch (IllegalStateException e) { + localVideoSender = null; + stopVideoCapturer(newVideoCapturer); + videoCapturer = oldVideoCapturer; + localVideoTrack = oldVideoTrack; + Log.e(TAG, "Video sender was disposed while changing video capturer.", e); + return; } + stopVideoCapturer(oldVideoCapturer); + + } + + private void stopVideoCapturer(@androidx.annotation.Nullable VideoCapturer capturer) { + try { + if (capturer != null) { + capturer.stopCapture(); + } + } catch (InterruptedException e) { + Log.e(TAG, "Could not stop video capturer.", e); + } } /** @@ -2286,8 +2313,9 @@ public void setVideoEnabled(final boolean enable) { public void toggleSendAudio(boolean enableAudio) { executor.execute(() -> { sendAudioEnabled = enableAudio; + boolean shouldSendAudio = enableAudio && config.audioCallEnabled; if (localAudioTrack != null) { - localAudioTrack.setEnabled(enableAudio); + localAudioTrack.setEnabled(shouldSendAudio); } }); } @@ -2307,8 +2335,10 @@ public void toggleSendVideo(boolean enableVideo) { changeVideoSource(StreamSource.FRONT_CAMERA); } else { changeVideoSource(StreamSource.CUSTOM); - blackFrameSender = new BlackFrameSender((CustomVideoCapturer) getVideoCapturer()); - blackFrameSender.start(); + if (!config.disableBlackFrameSender) { + blackFrameSender = new BlackFrameSender((CustomVideoCapturer) getVideoCapturer()); + blackFrameSender.start(); + } } }); } @@ -2499,7 +2529,22 @@ private VideoTrack createVideoTrack(VideoCapturer capturer) { private void findVideoSender(String streamId) { PeerConnection pc = getPeerConnectionFor(streamId); + findVideoSender(pc); + } + @androidx.annotation.Nullable + private RtpSender findVideoSender() { + for (PeerInfo peerInfo : peers.values()) { + RtpSender sender = findVideoSender(peerInfo.peerConnection); + if (sender != null) { + return sender; + } + } + return null; + } + + @androidx.annotation.Nullable + private RtpSender findVideoSender(@androidx.annotation.Nullable PeerConnection pc) { if (pc != null) { for (RtpSender sender : pc.getSenders()) { MediaStreamTrack track = sender.track(); @@ -2508,10 +2553,12 @@ private void findVideoSender(String streamId) { if (trackType.equals(VIDEO_TRACK_TYPE)) { Log.d(TAG, "Found video sender."); localVideoSender = sender; + return sender; } } } } + return null; } private static String getSdpVideoCodecName(String codec) { @@ -2833,6 +2880,14 @@ public void setDataChannelEnabled(boolean dataChannelEnabled) { this.config.dataChannelEnabled = dataChannelEnabled; } + public void setDisableBlackFrameSender(boolean disableBlackFrameSender) { + this.config.disableBlackFrameSender = disableBlackFrameSender; + } + + public void setDisableSilenceWhenMuted(boolean disableSilenceWhenMuted) { + this.config.disableSilenceWhenMuted = disableSilenceWhenMuted; + } + public void setFactory(@androidx.annotation.Nullable PeerConnectionFactory factory) { this.factory = factory; } diff --git a/webrtc-android-framework/src/test/java/io/antmedia/webrtcandroidframework/WebRTCClientTest.java b/webrtc-android-framework/src/test/java/io/antmedia/webrtcandroidframework/WebRTCClientTest.java index b59bc10b..2b9fcb76 100644 --- a/webrtc-android-framework/src/test/java/io/antmedia/webrtcandroidframework/WebRTCClientTest.java +++ b/webrtc-android-framework/src/test/java/io/antmedia/webrtcandroidframework/WebRTCClientTest.java @@ -1366,6 +1366,46 @@ public void testToggleSendAudioVideo() { }); } + @Test + public void testDisableBlackFrameSender_blackFrameSenderNotActivatedWhenDisabled() { + webRTCClient.setDisableBlackFrameSender(true); + webRTCClient.getConfig().videoCallEnabled = true; + + CustomVideoCapturer customVideoCapturerMock = mock(CustomVideoCapturer.class); + doNothing().when(customVideoCapturerMock).writeFrame(any()); + webRTCClient.setVideoCapturer(customVideoCapturerMock); + when(webRTCClient.createVideoCapturer(IWebRTCClient.StreamSource.CUSTOM)).thenReturn(customVideoCapturerMock); + + webRTCClient.toggleSendVideo(false); + + await().atMost(2, SECONDS).untilAsserted(() -> + assertNull(webRTCClient.getBlackFrameSender())); + assertTrue(webRTCClient.getConfig().videoSource == IWebRTCClient.StreamSource.CUSTOM); + } + + @Test + public void testDisableSilenceWhenMuted_doesNotDetachAudioSender() { + String streamId = "stream1"; + WebRTCClient.PeerInfo peerInfo = new WebRTCClient.PeerInfo(streamId, WebRTCClient.Mode.PUBLISH); + PeerConnection pc = mock(PeerConnection.class); + peerInfo.peerConnection = pc; + + RtpSender audioSender = mock(RtpSender.class); + AudioTrack audioTrackMock = mock(AudioTrack.class); + when(pc.getSenders()).thenReturn(Collections.singletonList(audioSender)); + + webRTCClient.getPeersForTest().put(streamId, peerInfo); + webRTCClient.setLocalAudioTrack(audioTrackMock); + webRTCClient.setDisableSilenceWhenMuted(true); + + webRTCClient.toggleSendAudio(false); + + await().atMost(2, SECONDS).untilAsserted(() -> { + verify(audioTrackMock).setEnabled(false); + verify(audioSender, never()).setTrack(null, false); + }); + } + @Test public void testTurnServer(){ String turnServerUri = "turn:example.antmedia.io"; diff --git a/webrtc-android-sample-app/src/main/java/io/antmedia/webrtc_android_sample_app/basic/ConferenceActivity.java b/webrtc-android-sample-app/src/main/java/io/antmedia/webrtc_android_sample_app/basic/ConferenceActivity.java index 4eb82de2..feb9b64d 100644 --- a/webrtc-android-sample-app/src/main/java/io/antmedia/webrtc_android_sample_app/basic/ConferenceActivity.java +++ b/webrtc-android-sample-app/src/main/java/io/antmedia/webrtc_android_sample_app/basic/ConferenceActivity.java @@ -162,6 +162,9 @@ public void createWebRTCClient(){ .setDataChannelObserver(createDatachannelObserver()) .build(); + webRTCClient.setDisableBlackFrameSender(true); + webRTCClient.setDisableSilenceWhenMuted(true); + joinButton = findViewById(R.id.join_conference_button); joinButton.setOnClickListener(v -> { joinLeaveRoom(); From 05ca5380a9903ead4c3d8be3825d43f4b1d197b8 Mon Sep 17 00:00:00 2001 From: USAMAWIZARD Date: Wed, 6 May 2026 01:18:51 +0530 Subject: [PATCH 2/4] resolve merge conf --- CRASH_FIX_SUMMARY.md | 136 ----------------------------------------- QUICK_FIX_REFERENCE.md | 98 ----------------------------- 2 files changed, 234 deletions(-) delete mode 100644 CRASH_FIX_SUMMARY.md delete mode 100644 QUICK_FIX_REFERENCE.md diff --git a/CRASH_FIX_SUMMARY.md b/CRASH_FIX_SUMMARY.md deleted file mode 100644 index f42ff875..00000000 --- a/CRASH_FIX_SUMMARY.md +++ /dev/null @@ -1,136 +0,0 @@ -# WebRTC Multiple Connection Reconnection Race Condition Fix - -## Problem Summary - -The application was experiencing native crashes (SIGSEGV - Signal 11) when multiple WebRTC connections attempted to reconnect simultaneously. The crash occurred in the native `libjingle_peerconnection_so.so` library on the `network_thread`. - -### Crash Details -- **Signal**: SIGSEGV (Segmentation Fault) -- **Location**: Native WebRTC library (`libjingle_peerconnection_so.so`) -- **Thread**: network_thread -- **Trigger**: Multiple WebRTC connections reconnecting at the same time - -### Root Cause Analysis - -The crash was caused by **critical race conditions** in the peer connection management code: - -1. **Unsynchronized Access**: Multiple threads (reconnection handlers, executor threads, network threads) were accessing `PeerInfo.peerConnection` without proper synchronization. - -2. **Dangling References**: After calling `pc.close()`, the `peerConnection` reference wasn't being set to `null`, allowing subsequent code to access closed connections. - -3. **Timing Issues**: The native `network_thread` in WebRTC was attempting to access peer connection objects while they were being closed on another thread. - -4. **Multiple Reconnection Handlers**: Three separate reconnection runnables (`publishReconnectorRunnable`, `playReconnectorRunnable`, `peerReconnectorRunnable`) could run concurrently and attempt to close/recreate the same connections. - -## Solution Implemented - -Added **comprehensive thread synchronization** using the `PeerInfo` object as a lock monitor for all operations that access or modify peer connections. - -### Changes Made - -#### 1. Reconnection Runnables (Lines 266-433) -- Added `synchronized (peerInfo)` blocks around all peer connection access -- Added `peerInfo.peerConnection = null` after closing connections -- Wrapped `pc.close()` in try-catch blocks to handle exceptions gracefully - -**Example:** -```java -synchronized (peerInfo) { - PeerConnection pc = peerInfo.peerConnection; - if (pc != null) { - try { - pc.close(); - } catch (Exception e) { - Log.e(TAG, "Error closing peer connection during reconnection", e); - } - peerInfo.peerConnection = null; - } - // ... reconnection logic -} -``` - -#### 2. Peer Connection Creation (Line 2196-2198) -- Added synchronization when assigning the peer connection reference - -```java -synchronized (peer) { - peer.peerConnection = peerConnection; -} -``` - -#### 3. SDP Observer Methods (Lines 705-759) -- Added synchronization in `onCreateSuccess()` callback -- Protected access to peer connection during SDP operations - -#### 4. WebSocket Connection Handler (Lines 1029-1038) -- Added synchronization when checking if peer connections need to be created - -#### 5. Configuration Handler (Lines 1635-1641) -- Added synchronization when checking for null peer connections in offer handling - -#### 6. ICE Candidate Handling (Lines 2526-2546) -- Added synchronization when adding remote ICE candidates - -#### 7. Cleanup Operations (Lines 2293-2322) -- Added synchronization in `closeInternal()` method -- Protected cleanup of senders, tracks, and data channels -- Added exception handling for cleanup operations - -#### 8. Helper Methods -- **`getPeerConnectionFor()`** (Lines 2886-2891): Added synchronization when accessing peer connection -- **`drainCandidates()`** (Lines 2862-2885): Added synchronization for the entire candidate draining process -- **`initDataChannel()`** (Lines 2262-2270): Added synchronization when creating data channels - -## Technical Details - -### Synchronization Strategy - -- **Lock Object**: Each `PeerInfo` instance is used as its own lock monitor -- **Granularity**: Fine-grained locking at the individual peer level (not global lock) -- **Benefits**: - - Allows concurrent operations on different peers - - Prevents race conditions on the same peer - - Minimal performance impact - -### Why This Fix Works - -1. **Atomicity**: Operations on peer connections are now atomic - close and null assignment happen together -2. **Visibility**: Synchronized blocks ensure memory visibility across threads -3. **Ordering**: Prevents reordering of operations that could lead to accessing closed connections -4. **Native Thread Safety**: Prevents the native `network_thread` from accessing objects being destroyed - -## Testing Recommendations - -1. **Stress Test**: Simulate multiple simultaneous connection drops and reconnections -2. **Conference Mode**: Test with multiple publish and play streams in conference mode -3. **Network Instability**: Test with unstable network conditions to trigger frequent reconnections -4. **Memory Profiling**: Ensure no memory leaks from the synchronization changes -5. **Performance Testing**: Verify that synchronization doesn't introduce performance degradation - -## Files Modified - -- `webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/core/WebRTCClient.java` - -## Build Status - -✅ Build successful - No compilation errors or lint issues - -## Additional Notes - -- The existing comment in the code (lines 289-291) mentions that using `dispose()` instead of `close()` causes segmentation faults. This fix addresses a related but different race condition. -- The fix is backward compatible and doesn't change the public API -- All exception handling has been added to prevent crashes from propagating - -## Prevention - -To prevent similar issues in the future: - -1. Always synchronize access to `peerInfo.peerConnection` -2. Set references to `null` after closing native objects -3. Use try-catch blocks around native operations -4. Consider the threading model when adding new peer connection operations - -## Related Issues - -This fix addresses the intermittent crash that occurred "time to time when multiple webrtc connection tries to reconnect at same time" as reported in the crash logs. - diff --git a/QUICK_FIX_REFERENCE.md b/QUICK_FIX_REFERENCE.md deleted file mode 100644 index d7c9dfcb..00000000 --- a/QUICK_FIX_REFERENCE.md +++ /dev/null @@ -1,98 +0,0 @@ -# Quick Reference: Race Condition Fix for Multiple WebRTC Reconnections - -## What Was Fixed - -Native crash (SIGSEGV) when multiple WebRTC connections reconnect simultaneously. - -## Key Pattern Applied - -### Before (Unsafe): -```java -PeerConnection pc = peerInfo.peerConnection; -if (pc != null) { - pc.close(); -} -// peerConnection reference still points to closed object! -``` - -### After (Thread-Safe): -```java -synchronized (peerInfo) { - PeerConnection pc = peerInfo.peerConnection; - if (pc != null) { - try { - pc.close(); - } catch (Exception e) { - Log.e(TAG, "Error closing peer connection", e); - } - peerInfo.peerConnection = null; // Clear reference - } -} -``` - -## Critical Changes Summary - -| Location | Change | Why | -|----------|--------|-----| -| Reconnection Runnables | Added `synchronized(peerInfo)` + null assignment | Prevent concurrent close/access | -| `createPeerConnectionInternal()` | Synchronized peer assignment | Prevent assignment during close | -| SDP Callbacks | Synchronized peer access | Prevent access to closing connection | -| `closeInternal()` | Synchronized cleanup loop | Prevent concurrent modification | -| `getPeerConnectionFor()` | Synchronized return | Ensure consistent view | -| `drainCandidates()` | Synchronized entire method | Prevent ICE candidate race | -| `initDataChannel()` | Synchronized channel creation | Prevent creation on closing peer | - -## Rule of Thumb - -**Always synchronize on `peerInfo` when:** -1. Accessing `peerInfo.peerConnection` -2. Modifying `peerInfo.peerConnection` -3. Calling methods on `peerInfo.peerConnection` -4. Closing or disposing peer connections - -## Pattern to Follow - -```java -PeerInfo peerInfo = getPeerInfoFor(streamId); -if (peerInfo != null) { - synchronized (peerInfo) { - PeerConnection pc = peerInfo.peerConnection; - if (pc != null) { - try { - // Do something with pc - pc.someMethod(); - } catch (Exception e) { - Log.e(TAG, "Error", e); - } - } - } -} -``` - -## What NOT to Do - -❌ **Don't**: Access `peerConnection` without synchronization -❌ **Don't**: Keep references to `PeerConnection` outside synchronized blocks -❌ **Don't**: Call `close()` without setting the reference to null -❌ **Don't**: Assume the native object is valid after getting the reference - -## Testing Checklist - -- [ ] Multiple connections reconnecting simultaneously -- [ ] Conference mode with many participants -- [ ] Rapid connect/disconnect cycles -- [ ] Network instability simulation -- [ ] Memory leak check (no dangling references) - -## Build Verification - -```bash -./gradlew :webrtc-android-framework:assembleDebug -``` - -Expected: ✅ BUILD SUCCESSFUL - -## Branch - -`improveReconnection` - From a5f21c12952ecb33c4ebcb2d116ac50c8d82c58f Mon Sep 17 00:00:00 2001 From: USAMAWIZARD Date: Wed, 6 May 2026 01:19:26 +0530 Subject: [PATCH 3/4] resolve merge conf --- .project | 28 ---------------------- .settings/org.eclipse.buildship.core.prefs | 13 ---------- 2 files changed, 41 deletions(-) delete mode 100644 .project delete mode 100644 .settings/org.eclipse.buildship.core.prefs diff --git a/.project b/.project deleted file mode 100644 index e7bd531d..00000000 --- a/.project +++ /dev/null @@ -1,28 +0,0 @@ - - - WebRTC-Android-SDK - Project WebRTC-Android-SDK created by Buildship. - - - - - org.eclipse.buildship.core.gradleprojectbuilder - - - - - - org.eclipse.buildship.core.gradleprojectnature - - - - 1737395867684 - - 30 - - org.eclipse.core.resources.regexFilterMatcher - node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ - - - - diff --git a/.settings/org.eclipse.buildship.core.prefs b/.settings/org.eclipse.buildship.core.prefs deleted file mode 100644 index e583fcee..00000000 --- a/.settings/org.eclipse.buildship.core.prefs +++ /dev/null @@ -1,13 +0,0 @@ -arguments=--init-script /home/usama/.local/share/nvim/mason/packages/jdtls/config_linux/org.eclipse.osgi/58/0/.cp/gradle/init/init.gradle -auto.sync=false -build.scans.enabled=false -connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) -connection.project.dir= -eclipse.preferences.version=1 -gradle.user.home= -java.home=/usr/lib/jvm/java-17-openjdk -jvm.arguments= -offline.mode=false -override.workspace.settings=true -show.console.view=true -show.executions.view=true From 2054745a1c8b0362eff230b013335a424880e5bb Mon Sep 17 00:00:00 2001 From: USAMAWIZARD Date: Wed, 6 May 2026 01:44:58 +0530 Subject: [PATCH 4/4] stop the audio video tracks when disabled to stop sending rtp data to the server --- .../core/WebRTCClient.java | 98 ++++++++++++++----- .../WebRTCClientTest.java | 40 +++++++- 2 files changed, 112 insertions(+), 26 deletions(-) diff --git a/webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/core/WebRTCClient.java b/webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/core/WebRTCClient.java index 3263bdbd..247d6ffb 100644 --- a/webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/core/WebRTCClient.java +++ b/webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/core/WebRTCClient.java @@ -2160,15 +2160,21 @@ public void initDataChannel(String streamId) { } public void setDegradationPreference(RtpParameters.DegradationPreference degradationPreference) { - if (localVideoSender == null) { - Log.w(TAG, "Sender is not ready."); - return; - } executor.execute(() -> { - RtpParameters newParameters = localVideoSender.getParameters(); - if (newParameters != null) { - newParameters.degradationPreference = degradationPreference; - localVideoSender.setParameters(newParameters); + RtpSender videoSender = findVideoSender(); + if (videoSender == null) { + Log.w(TAG, "Sender is not ready."); + return; + } + try { + RtpParameters newParameters = videoSender.getParameters(); + if (newParameters != null) { + newParameters.degradationPreference = degradationPreference; + videoSender.setParameters(newParameters); + } + } catch (IllegalStateException e) { + localVideoSender = null; + Log.w(TAG, "Video sender is not available: " + e.getMessage()); } }); } @@ -2289,8 +2295,12 @@ public void setAudioEnabled(final boolean enable) { config.audioCallEnabled = enable; executor.execute(() -> { + boolean shouldSendAudio = enable && sendAudioEnabled; if (localAudioTrack != null) { - localAudioTrack.setEnabled(enable); + localAudioTrack.setEnabled(shouldSendAudio); + } + if (config.disableSilenceWhenMuted) { + setAudioSenderEnabled(shouldSendAudio); } }); } @@ -2317,6 +2327,9 @@ public void toggleSendAudio(boolean enableAudio) { if (localAudioTrack != null) { localAudioTrack.setEnabled(shouldSendAudio); } + if (config.disableSilenceWhenMuted) { + setAudioSenderEnabled(shouldSendAudio); + } }); } @@ -2474,28 +2487,32 @@ public void setVideoMaxBitrate(@androidx.annotation.Nullable final Integer maxBi return; } executor.execute(() -> { - if (localVideoSender == null) { - return; - } Log.d(TAG, "Requested max video bitrate: " + maxBitrateKbps); - if (localVideoSender == null) { + RtpSender videoSender = findVideoSender(); + if (videoSender == null) { Log.w(TAG, "Sender is not ready."); return; } - RtpParameters parameters = localVideoSender.getParameters(); - if (parameters.encodings.isEmpty()) { - Log.w(TAG, "RtpParameters are not ready."); - return; - } + try { + RtpParameters parameters = videoSender.getParameters(); + if (parameters.encodings.isEmpty()) { + Log.w(TAG, "RtpParameters are not ready."); + return; + } - for (RtpParameters.Encoding encoding : parameters.encodings) { - // Null value means no limit. - encoding.maxBitrateBps = maxBitrateKbps == null ? null : maxBitrateKbps * BPS_IN_KBPS; - encoding.minBitrateBps = maxBitrateKbps == null ? null : maxBitrateKbps * BPS_IN_KBPS / 2; - } - if (!localVideoSender.setParameters(parameters)) { - Log.e(TAG, "RtpSender.setParameters failed."); + for (RtpParameters.Encoding encoding : parameters.encodings) { + // Null value means no limit. + encoding.maxBitrateBps = maxBitrateKbps == null ? null : maxBitrateKbps * BPS_IN_KBPS; + encoding.minBitrateBps = maxBitrateKbps == null ? null : maxBitrateKbps * BPS_IN_KBPS / 2; + } + if (!videoSender.setParameters(parameters)) { + Log.e(TAG, "RtpSender.setParameters failed."); + } + } catch (IllegalStateException e) { + localVideoSender = null; + Log.w(TAG, "Video sender is not available: " + e.getMessage()); + return; } Log.d(TAG, "Configured max video bitrate to: " + maxBitrateKbps); }); @@ -2532,6 +2549,37 @@ private void findVideoSender(String streamId) { findVideoSender(pc); } + private void setAudioSenderEnabled(boolean enabled) { + for (PeerInfo peerInfo : peers.values()) { + PeerConnection pc = peerInfo.peerConnection; + if (pc == null) { + continue; + } + try { + for (RtpSender sender : pc.getSenders()) { + MediaStreamTrack track = sender.track(); + if (track == null || !MediaStreamTrack.AUDIO_TRACK_KIND.equals(track.kind())) { + continue; + } + RtpParameters parameters = sender.getParameters(); + if (parameters == null || parameters.encodings.isEmpty()) { + Log.w(TAG, "Audio RtpParameters are not ready."); + return; + } + for (RtpParameters.Encoding encoding : parameters.encodings) { + encoding.active = enabled; + } + if (!sender.setParameters(parameters)) { + Log.e(TAG, "Audio RtpSender.setParameters failed."); + } + return; + } + } catch (IllegalStateException e) { + Log.w(TAG, "Audio sender is not available: " + e.getMessage()); + } + } + } + @androidx.annotation.Nullable private RtpSender findVideoSender() { for (PeerInfo peerInfo : peers.values()) { diff --git a/webrtc-android-framework/src/test/java/io/antmedia/webrtcandroidframework/WebRTCClientTest.java b/webrtc-android-framework/src/test/java/io/antmedia/webrtcandroidframework/WebRTCClientTest.java index 2b9fcb76..ea291f92 100644 --- a/webrtc-android-framework/src/test/java/io/antmedia/webrtcandroidframework/WebRTCClientTest.java +++ b/webrtc-android-framework/src/test/java/io/antmedia/webrtcandroidframework/WebRTCClientTest.java @@ -1055,6 +1055,14 @@ public void testDegradationPreference() { verify(sender, never()).setParameters(any()); webRTCClient.localVideoSender = sender; + PeerConnection pc = mock(PeerConnection.class); + MediaStreamTrack videoTrack = mock(MediaStreamTrack.class); + when(videoTrack.kind()).thenReturn("video"); + when(sender.track()).thenReturn(videoTrack); + when(pc.getSenders()).thenReturn(Collections.singletonList(sender)); + WebRTCClient.PeerInfo peerInfo = new WebRTCClient.PeerInfo("stream1", WebRTCClient.Mode.PUBLISH); + peerInfo.peerConnection = pc; + webRTCClient.getPeersForTest().put("stream1", peerInfo); RtpParameters parameters = mock(RtpParameters.class); when(sender.getParameters()).thenReturn(parameters); webRTCClient.setDegradationPreference(degradationPreference); @@ -1077,6 +1085,14 @@ public void testSetVideoMaxBitrate() throws NoSuchFieldException, IllegalAccessE verify(sender, never()).setParameters(any()); webRTCClient.localVideoSender = sender; + PeerConnection pc = mock(PeerConnection.class); + MediaStreamTrack videoTrack = mock(MediaStreamTrack.class); + when(videoTrack.kind()).thenReturn("video"); + when(sender.track()).thenReturn(videoTrack); + when(pc.getSenders()).thenReturn(Collections.singletonList(sender)); + WebRTCClient.PeerInfo peerInfo = new WebRTCClient.PeerInfo("stream1", WebRTCClient.Mode.PUBLISH); + peerInfo.peerConnection = pc; + webRTCClient.getPeersForTest().put("stream1", peerInfo); RtpParameters.Encoding encodings = mock(RtpParameters.Encoding.class); List mockEncoding = Collections.emptyList(); @@ -1384,7 +1400,7 @@ public void testDisableBlackFrameSender_blackFrameSenderNotActivatedWhenDisabled } @Test - public void testDisableSilenceWhenMuted_doesNotDetachAudioSender() { + public void testDisableSilenceWhenMuted_doesNotDetachAudioSender() throws NoSuchFieldException, IllegalAccessException { String streamId = "stream1"; WebRTCClient.PeerInfo peerInfo = new WebRTCClient.PeerInfo(streamId, WebRTCClient.Mode.PUBLISH); PeerConnection pc = mock(PeerConnection.class); @@ -1392,8 +1408,20 @@ public void testDisableSilenceWhenMuted_doesNotDetachAudioSender() { RtpSender audioSender = mock(RtpSender.class); AudioTrack audioTrackMock = mock(AudioTrack.class); + RtpParameters parameters = mock(RtpParameters.class); + RtpParameters.Encoding encoding = new RtpParameters.Encoding(null, true, null); + List encodings = new ArrayList<>(); + encodings.add(encoding); + when(audioTrackMock.kind()).thenReturn(MediaStreamTrack.AUDIO_TRACK_KIND); + when(audioSender.track()).thenReturn(audioTrackMock); + when(audioSender.getParameters()).thenReturn(parameters); + when(audioSender.setParameters(parameters)).thenReturn(true); when(pc.getSenders()).thenReturn(Collections.singletonList(audioSender)); + Field field = RtpParameters.class.getDeclaredField("encodings"); + field.setAccessible(true); + field.set(parameters, encodings); + webRTCClient.getPeersForTest().put(streamId, peerInfo); webRTCClient.setLocalAudioTrack(audioTrackMock); webRTCClient.setDisableSilenceWhenMuted(true); @@ -1403,6 +1431,16 @@ public void testDisableSilenceWhenMuted_doesNotDetachAudioSender() { await().atMost(2, SECONDS).untilAsserted(() -> { verify(audioTrackMock).setEnabled(false); verify(audioSender, never()).setTrack(null, false); + verify(audioSender).setParameters(parameters); + assertFalse(encoding.active); + }); + + webRTCClient.toggleSendAudio(true); + + await().atMost(2, SECONDS).untilAsserted(() -> { + verify(audioTrackMock).setEnabled(true); + verify(audioSender, times(2)).setParameters(parameters); + assertTrue(encoding.active); }); }