diff --git a/android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java b/android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java index c17254a8b..731ee8e5c 100644 --- a/android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java +++ b/android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java @@ -285,6 +285,17 @@ void mediaStreamTrackSetEnabled(String trackId, final boolean enabled) { } } + void disposeAllTracks() { + for (Map.Entry entry : tracks.entrySet()) { + try { + entry.getValue().dispose(); + } catch (Exception e) { + Log.w(TAG, "disposeAllTracks: error disposing " + entry.getKey(), e); + } + } + tracks.clear(); + } + void disposeTrack(String id) { TrackPrivate track = tracks.remove(id); if (track != null) { @@ -344,6 +355,14 @@ public void run() { } private void createScreenStream() { + // Guards against onServiceConnected firing after invalidate() has disposed and nulled mFactory. + if (webRTCModule.mFactory == null) { + if (displayMediaPromise != null) { + displayMediaPromise.reject("ERR_MODULE_DISPOSED", "WebRTCModule disposed during getDisplayMedia"); + displayMediaPromise = null; + } + return; + } VideoTrack track = createScreenTrack(); if (track == null) { @@ -614,9 +633,10 @@ public void dispose() { } } - // Clean up VideoTrackAdapter for video tracks - if (!isClone && videoTrackAdapter != null && track instanceof VideoTrack) { + // Clean up VideoTrackAdapter for video tracks (each TrackPrivate, incl. clones, has its own) + if (videoTrackAdapter != null && track instanceof VideoTrack) { videoTrackAdapter.removeDimensionDetector((VideoTrack) track); + videoTrackAdapter.dispose(); } /* diff --git a/android/src/main/java/com/oney/WebRTCModule/PeerConnectionObserver.java b/android/src/main/java/com/oney/WebRTCModule/PeerConnectionObserver.java index 32b028f82..6ba19a041 100644 --- a/android/src/main/java/com/oney/WebRTCModule/PeerConnectionObserver.java +++ b/android/src/main/java/com/oney/WebRTCModule/PeerConnectionObserver.java @@ -99,6 +99,8 @@ void dispose() { // by the PeerConnection instance (RtpReceivers, RtpSenders, etc.) peerConnection.dispose(); + videoTrackAdapters.dispose(); + remoteStreamIds.clear(); remoteStreams.clear(); remoteTracks.clear(); diff --git a/android/src/main/java/com/oney/WebRTCModule/VideoTrackAdapter.java b/android/src/main/java/com/oney/WebRTCModule/VideoTrackAdapter.java index 486bfe6b7..946dfee55 100644 --- a/android/src/main/java/com/oney/WebRTCModule/VideoTrackAdapter.java +++ b/android/src/main/java/com/oney/WebRTCModule/VideoTrackAdapter.java @@ -89,6 +89,10 @@ public void removeDimensionDetector(VideoTrack videoTrack) { Log.d(TAG, "Deleted dimension detector for " + trackId); } + void dispose() { + timer.cancel(); + } + /** * Implements 'mute'/'unmute' events for remote video tracks through * the {@link VideoSink} interface. diff --git a/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java b/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java index 089f6150a..d2cf9ea40 100644 --- a/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java +++ b/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java @@ -124,6 +124,53 @@ public WebRTCModule(ReactApplicationContext reactContext) { getUserMediaImpl = new GetUserMediaImpl(this, reactContext); } + @Override + public void invalidate() { + Log.d(TAG, "invalidate()"); + + try { + ThreadUtils.submitToExecutor(() -> { + // 1. Dispose PeerConnections (dispose() calls close() internally) + for (int i = 0; i < mPeerConnectionObservers.size(); i++) { + try { + mPeerConnectionObservers.valueAt(i).dispose(); + } catch (Exception e) { + Log.w(TAG, "invalidate(): error disposing PC " + mPeerConnectionObservers.keyAt(i), e); + } + } + mPeerConnectionObservers.clear(); + + // 2. Detach tracks, then dispose streams. Tracks themselves get disposed in step 3. + for (Map.Entry entry : localStreams.entrySet()) { + try { + MediaStream stream = entry.getValue(); + for (AudioTrack t : new ArrayList<>(stream.audioTracks)) stream.removeTrack(t); + for (VideoTrack t : new ArrayList<>(stream.videoTracks)) stream.removeTrack(t); + stream.dispose(); + } catch (Exception e) { + Log.w(TAG, "invalidate(): error disposing stream " + entry.getKey(), e); + } + } + localStreams.clear(); + + // 3. Stop capturers + dispose tracks (prevents use-after-free on factory threads) + getUserMediaImpl.disposeAllTracks(); + + // 4. Dispose factory (frees C++ factory + 3 threads) + if (mFactory != null) { + mFactory.dispose(); + mFactory = null; + } + + return null; + }).get(); + } catch (InterruptedException | ExecutionException e) { + Log.e(TAG, "invalidate() error", e); + } + + super.invalidate(); + } + private JavaAudioDeviceModule createAudioDeviceModule(ReactApplicationContext reactContext) { speechActivityDetector = new SpeechActivityDetector(new SpeechActivityDetector.Listener() { @Override diff --git a/package-lock.json b/package-lock.json index 8f3a70152..8ef8e5ddd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@stream-io/react-native-webrtc", - "version": "137.1.4-alpha.5", + "version": "137.1.4-alpha.8", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@stream-io/react-native-webrtc", - "version": "137.1.4-alpha.5", + "version": "137.1.4-alpha.8", "license": "MIT", "dependencies": { "base64-js": "1.5.1", diff --git a/package.json b/package.json index ba8cc44c3..84608304c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@stream-io/react-native-webrtc", - "version": "137.1.4-alpha.5", + "version": "137.1.4-alpha.8", "repository": { "type": "git", "url": "git+https://github.com/GetStream/react-native-webrtc.git"