From 6b1128292d2a1948ea0ce4c7edacd5c4baf1f1a0 Mon Sep 17 00:00:00 2001 From: Denis Koltovich Date: Wed, 15 Apr 2026 10:51:42 +0300 Subject: [PATCH 1/2] fix: Check null before use objects and add runOnExecutor --- .../com/oney/WebRTCModule/WebRTCModule.java | 134 +++++++++++------- 1 file changed, 82 insertions(+), 52 deletions(-) diff --git a/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java b/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java index 7b540f6f2..b8c2d2149 100644 --- a/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java +++ b/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java @@ -518,6 +518,11 @@ void observeVoiceActivity() { for (int i = 0; i < mPeerConnectionObservers.size(); i++) { int key = mPeerConnectionObservers.keyAt(i); PeerConnectionObserver peer = mPeerConnectionObservers.get(key); + + if (peer == null) { + continue; + } + silenceIncomingCount.put(peer.getId(), 0); } @@ -527,76 +532,97 @@ void observeVoiceActivity() { WebRTCModule.voiceTimer.schedule(new TimerTask() { @Override public void run() { - for (int i = 0; i < mPeerConnectionObservers.size(); i++) { - int key = mPeerConnectionObservers.keyAt(i); - PeerConnectionObserver peer = mPeerConnectionObservers.get(key); - int id = peer.getId(); - - if (peer.getPeerConnection().connectionState() == PeerConnection.PeerConnectionState.CONNECTED) { - peer.getPeerConnection().getStats(new RTCStatsCollectorCallback() { - @Override - public void onStatsDelivered(RTCStatsReport rtcStatsReport) { - - for (RTCStats stats : rtcStatsReport.getStatsMap().values()) { - if (stats.getType().equals("inbound-rtp")) { - Object audioLevelObject = stats.getMembers().get("audioLevel"); - - if (audioLevelObject instanceof Double) { - Double audioLevel = ((Double) audioLevelObject); - - if (audioLevel > 0.025) { - silenceIncomingCount.put(id, 0); - incomingAudioLevelHolder.setValue(peer, true, audioLevel.doubleValue()); - } else { - silenceIncomingCount.put(id, silenceIncomingCount.get(id) + 1); - - if (silenceIncomingCount.get(id) > 4.0) { - incomingAudioLevelHolder.setValue(peer, false, 0); + ThreadUtils.runOnExecutor(() -> { + for (int i = 0; i < mPeerConnectionObservers.size(); i++) { + int key = mPeerConnectionObservers.keyAt(i); + PeerConnectionObserver peer = mPeerConnectionObservers.get(key); + + if (peer == null) { + continue; + } + + int id = peer.getId(); + + PeerConnection peerConnection = peer.getPeerConnection(); + + if (peerConnection == null) { + continue; + } + + if (peerConnection.connectionState() == PeerConnection.PeerConnectionState.CONNECTED) { + peerConnection.getStats(new RTCStatsCollectorCallback() { + @Override + public void onStatsDelivered(RTCStatsReport rtcStatsReport) { + + for (RTCStats stats : rtcStatsReport.getStatsMap().values()) { + if (stats.getType().equals("inbound-rtp")) { + Object audioLevelObject = stats.getMembers().get("audioLevel"); + + if (audioLevelObject instanceof Double) { + Double audioLevel = ((Double) audioLevelObject); + + if (audioLevel > 0.025) { + silenceIncomingCount.put(id, 0); + incomingAudioLevelHolder.setValue(peer, true, audioLevel.doubleValue()); + } else { + silenceIncomingCount.put(id, silenceIncomingCount.get(id) + 1); + + if (silenceIncomingCount.get(id) > 4.0) { + incomingAudioLevelHolder.setValue(peer, false, 0); + } } } } } } - } - }); + }); + } } - } - for (int i = 0; i < mPeerConnectionObservers.size(); i++) { - int key = mPeerConnectionObservers.keyAt(i); - PeerConnectionObserver peer = mPeerConnectionObservers.get(key); + for (int i = 0; i < mPeerConnectionObservers.size(); i++) { + int key = mPeerConnectionObservers.keyAt(i); + PeerConnectionObserver peer = mPeerConnectionObservers.get(key); + if (peer == null) { + continue; + } + + PeerConnection peerConnection = peer.getPeerConnection(); + if (peerConnection == null) { + continue; + } - if (peer.getPeerConnection().connectionState() == PeerConnection.PeerConnectionState.CONNECTED) { - peer.getPeerConnection().getStats(new RTCStatsCollectorCallback() { - @Override - public void onStatsDelivered(RTCStatsReport rtcStatsReport) { + if (peerConnection.connectionState() == PeerConnection.PeerConnectionState.CONNECTED) { + peerConnection.getStats(new RTCStatsCollectorCallback() { + @Override + public void onStatsDelivered(RTCStatsReport rtcStatsReport) { - for (RTCStats stats : rtcStatsReport.getStatsMap().values()) { - if (stats.getType().equals("media-source")) { - Object audioLevelObject = stats.getMembers().get("audioLevel"); + for (RTCStats stats : rtcStatsReport.getStatsMap().values()) { + if (stats.getType().equals("media-source")) { + Object audioLevelObject = stats.getMembers().get("audioLevel"); - if (audioLevelObject instanceof Double) { - Double audioLevel = ((Double) audioLevelObject); + if (audioLevelObject instanceof Double) { + Double audioLevel = ((Double) audioLevelObject); - if (audioLevel > 0.01) { - silenceOutgoingCount[0] = 0; - outgoingAudioLevelHolder.setValue(peer, true, audioLevel.doubleValue()); - } else { - silenceOutgoingCount[0] += 1; + if (audioLevel > 0.01) { + silenceOutgoingCount[0] = 0; + outgoingAudioLevelHolder.setValue(peer, true, audioLevel.doubleValue()); + } else { + silenceOutgoingCount[0] += 1; - if (silenceOutgoingCount[0] > 4.0) { - outgoingAudioLevelHolder.setValue(peer, false, 0); + if (silenceOutgoingCount[0] > 4.0) { + outgoingAudioLevelHolder.setValue(peer, false, 0); + } } } } } } - } - }); + }); - return; + return; + } } - } + }); } }, 0, checkInterval); @@ -1542,7 +1568,11 @@ public void peerConnectionDispose(int id) { if (pco == null || pco.getPeerConnection() == null) { Log.d(TAG, "peerConnectionDispose() peerConnection is null"); } - pco.dispose(); + + if (pco != null) { + pco.dispose(); + } + mPeerConnectionObservers.remove(id); if (mPeerConnectionObservers.size() == 0) { From 480ec17bb3073570e728d9e49411af88d5fbe102 Mon Sep 17 00:00:00 2001 From: Denis Koltovich Date: Wed, 15 Apr 2026 18:16:40 +0300 Subject: [PATCH 2/2] fix: Comments --- .../WebRTCModule/PeerConnectionObserver.java | 16 +++++- .../com/oney/WebRTCModule/WebRTCModule.java | 49 ++++++++++++++----- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/android/src/main/java/com/oney/WebRTCModule/PeerConnectionObserver.java b/android/src/main/java/com/oney/WebRTCModule/PeerConnectionObserver.java index 8652b1ccc..0f41ab1e0 100644 --- a/android/src/main/java/com/oney/WebRTCModule/PeerConnectionObserver.java +++ b/android/src/main/java/com/oney/WebRTCModule/PeerConnectionObserver.java @@ -69,12 +69,24 @@ void setPeerConnection(PeerConnection peerConnection) { void close() { Log.d(TAG, "PeerConnection.close() for " + id); - peerConnection.close(); + PeerConnection pc = peerConnection; + if (pc == null) { + return; + } + + pc.close(); } void dispose() { Log.d(TAG, "PeerConnection.dispose() for " + id); + PeerConnection pc = peerConnection; + if (pc == null) { + return; + } + // Make future calls observe disposal immediately. + peerConnection = null; + // Remove video track adapters for (MediaStreamTrack track : this.remoteTracks.values()) { if (track instanceof VideoTrack) { @@ -91,7 +103,7 @@ void dispose() { // At this point there should be no local MediaStreams in the associated // PeerConnection. Call dispose() to free all remaining resources held // by the PeerConnection instance (RtpReceivers, RtpSenders, etc.) - peerConnection.dispose(); + pc.dispose(); remoteStreamIds.clear(); remoteStreams.clear(); diff --git a/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java b/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java index b8c2d2149..b5d1060ce 100644 --- a/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java +++ b/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java @@ -43,6 +43,9 @@ import java.util.TimerTask; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; interface OnValueChangeListener { void onValueChanged(PeerConnectionObserver peerConnection, boolean isSpeaking, double audioLevel); @@ -101,6 +104,10 @@ public class WebRTCModule extends ReactContextBaseJavaModule { AudioLevelValueHolder incomingAudioLevelHolder; AudioLevelValueHolder outgoingAudioLevelHolder; + private final AtomicInteger voicePollGeneration = new AtomicInteger(0); + private final AtomicReference> voicePollTaskRef = new AtomicReference<>(null); + + private final GetUserMediaImpl getUserMediaImpl; public WebRTCModule(ReactApplicationContext reactContext) { @@ -499,6 +506,14 @@ public boolean peerConnectionInit(ReadableMap configuration, int id) { } void cancelVoiceActivity() { + // Invalidate all queued/running poll tasks from previous timer cycle. + voicePollGeneration.incrementAndGet(); + + Future pending = voicePollTaskRef.getAndSet(null); + if (pending != null) { + pending.cancel(false); + } + if (WebRTCModule.voiceTimer != null) { WebRTCModule.voiceTimer.cancel(); WebRTCModule.voiceTimer.purge(); @@ -509,6 +524,7 @@ void cancelVoiceActivity() { outgoingAudioLevelHolder.cleanup(); } + void observeVoiceActivity() { int checkInterval = 300; @@ -518,42 +534,50 @@ void observeVoiceActivity() { for (int i = 0; i < mPeerConnectionObservers.size(); i++) { int key = mPeerConnectionObservers.keyAt(i); PeerConnectionObserver peer = mPeerConnectionObservers.get(key); - - if (peer == null) { - continue; + if (peer != null) { + silenceIncomingCount.put(peer.getId(), 0); } - - silenceIncomingCount.put(peer.getId(), 0); } cancelVoiceActivity(); + final int generation = voicePollGeneration.incrementAndGet(); WebRTCModule.voiceTimer = new Timer(); WebRTCModule.voiceTimer.schedule(new TimerTask() { @Override public void run() { - ThreadUtils.runOnExecutor(() -> { + if (voicePollGeneration.get() != generation) { + return; + } + + Future current = voicePollTaskRef.get(); + if (current != null && !current.isDone()) { + return; // previous tick still queued/running + } + + Future next = ThreadUtils.submitToExecutor(() -> { + if (voicePollGeneration.get() != generation) { + return; + } + for (int i = 0; i < mPeerConnectionObservers.size(); i++) { int key = mPeerConnectionObservers.keyAt(i); PeerConnectionObserver peer = mPeerConnectionObservers.get(key); - if (peer == null) { continue; } - int id = peer.getId(); - PeerConnection peerConnection = peer.getPeerConnection(); - if (peerConnection == null) { continue; } + int id = peer.getId(); + if (peerConnection.connectionState() == PeerConnection.PeerConnectionState.CONNECTED) { peerConnection.getStats(new RTCStatsCollectorCallback() { @Override public void onStatsDelivered(RTCStatsReport rtcStatsReport) { - for (RTCStats stats : rtcStatsReport.getStatsMap().values()) { if (stats.getType().equals("inbound-rtp")) { Object audioLevelObject = stats.getMembers().get("audioLevel"); @@ -595,7 +619,6 @@ public void onStatsDelivered(RTCStatsReport rtcStatsReport) { peerConnection.getStats(new RTCStatsCollectorCallback() { @Override public void onStatsDelivered(RTCStatsReport rtcStatsReport) { - for (RTCStats stats : rtcStatsReport.getStatsMap().values()) { if (stats.getType().equals("media-source")) { Object audioLevelObject = stats.getMembers().get("audioLevel"); @@ -623,6 +646,8 @@ public void onStatsDelivered(RTCStatsReport rtcStatsReport) { } } }); + + voicePollTaskRef.set(next); } }, 0, checkInterval);