diff --git a/android/src/main/java/com/oney/WebRTCModule/CameraCaptureController.java b/android/src/main/java/com/oney/WebRTCModule/CameraCaptureController.java index 939d79350..020f0972c 100644 --- a/android/src/main/java/com/oney/WebRTCModule/CameraCaptureController.java +++ b/android/src/main/java/com/oney/WebRTCModule/CameraCaptureController.java @@ -119,9 +119,11 @@ public void applyConstraints(ReadableMap constraints, @Nullable Consumer processors = names.toArrayList() @@ -602,10 +607,15 @@ void setVideoEffects(String trackId, ReadableArray names) { VideoEffectProcessor videoEffectProcessor = new VideoEffectProcessor(processors, surfaceTextureHelper); videoSource.setVideoProcessor(videoEffectProcessor); + track.videoEffectProcessor = videoEffectProcessor; } else { videoSource.setVideoProcessor(null); } + + if (previousProcessor != null) { + previousProcessor.dispose(); + } } } @@ -643,6 +653,9 @@ private static class TrackPrivate { */ public final VideoTrackAdapter videoTrackAdapter; + /** Current effect processor, disposed on filter switch and on track teardown. */ + public VideoEffectProcessor videoEffectProcessor; + /** * Whether this object has been disposed or not. */ @@ -693,6 +706,13 @@ public void dispose() { } } + // After stopCapture so no frame can still reach it; before + // surfaceTextureHelper dispose so GL is still alive for cleanup. + if (!isClone && videoEffectProcessor != null) { + videoEffectProcessor.dispose(); + videoEffectProcessor = null; + } + // Clean up VideoTrackAdapter for video tracks (each TrackPrivate, incl. clones, has its own) if (videoTrackAdapter != null && track instanceof VideoTrack) { videoTrackAdapter.removeDimensionDetector((VideoTrack) track); diff --git a/android/src/main/java/com/oney/WebRTCModule/videoEffects/VideoEffectProcessor.java b/android/src/main/java/com/oney/WebRTCModule/videoEffects/VideoEffectProcessor.java index 4edd62680..bb1269fac 100644 --- a/android/src/main/java/com/oney/WebRTCModule/videoEffects/VideoEffectProcessor.java +++ b/android/src/main/java/com/oney/WebRTCModule/videoEffects/VideoEffectProcessor.java @@ -15,6 +15,7 @@ public class VideoEffectProcessor implements VideoProcessor { private VideoSink mSink; final private SurfaceTextureHelper textureHelper; final private List videoFrameProcessors; + private boolean disposed = false; public VideoEffectProcessor(List processors, SurfaceTextureHelper textureHelper) { this.textureHelper = textureHelper; @@ -27,6 +28,21 @@ public void onCapturerStarted(boolean success) {} @Override public void onCapturerStopped() {} + /** + * Disposes each wrapped processor. Posted to the capturer handler so it runs + * after any in-flight frame, so implementations can clean up GL state inline. + * Idempotent. Not wired to {@link #onCapturerStopped} because that also fires on pause. + */ + public void dispose() { + textureHelper.getHandler().post(() -> { + if (disposed) return; + disposed = true; + for (VideoFrameProcessor processor : this.videoFrameProcessors) { + processor.dispose(); + } + }); + } + @Override public void setSink(VideoSink sink) { mSink = sink; diff --git a/android/src/main/java/com/oney/WebRTCModule/videoEffects/VideoFrameProcessor.java b/android/src/main/java/com/oney/WebRTCModule/videoEffects/VideoFrameProcessor.java index b0a4c0cec..2a5c40706 100644 --- a/android/src/main/java/com/oney/WebRTCModule/videoEffects/VideoFrameProcessor.java +++ b/android/src/main/java/com/oney/WebRTCModule/videoEffects/VideoFrameProcessor.java @@ -16,4 +16,10 @@ public interface VideoFrameProcessor { * @return processed videoframe which will rendered */ public VideoFrame process(VideoFrame frame, SurfaceTextureHelper textureHelper); + + /** + * Called once when this processor leaves the pipeline. Release any native or + * GL resources here. Runs on the capturer handler. Default no-op. + */ + default void dispose() {} } diff --git a/ios/RCTWebRTC/VideoCaptureController.m b/ios/RCTWebRTC/VideoCaptureController.m index 8ae5ce194..483e78a40 100644 --- a/ios/RCTWebRTC/VideoCaptureController.m +++ b/ios/RCTWebRTC/VideoCaptureController.m @@ -161,6 +161,32 @@ - (void)determineDevice:(NSDictionary *)constraints { - (void)applyConstraints:(NSDictionary *)constraints error:(NSError **)outError { BOOL hasChanged = NO; + // Re-read device-selecting constraints so `MediaStreamTrack._switchCamera()` + // can flip the camera via `applyConstraints({facingMode})` — the documented + // W3C pattern that browsers also implement. + NSString *deviceId = constraints[@"deviceId"]; + id facingMode = constraints[@"facingMode"]; + + BOOL targetFront = self.usingFrontCamera; + if ([facingMode isKindOfClass:[NSString class]]) { + if ([facingMode isEqualToString:@"environment"]) { + targetFront = NO; + } else if ([facingMode isEqualToString:@"user"]) { + targetFront = YES; + } + } + if (targetFront != self.usingFrontCamera) { + self.usingFrontCamera = targetFront; + // Clear deviceId so `startCapture` re-resolves it from the new position. + self.deviceId = nil; + hasChanged = YES; + } + + if (deviceId && ![deviceId isEqualToString:self.deviceId]) { + self.deviceId = deviceId; + hasChanged = YES; + } + int width = [constraints[@"width"] intValue]; int height = [constraints[@"height"] intValue]; int frameRate = [constraints[@"frameRate"] intValue]; diff --git a/package-lock.json b/package-lock.json index 6ea350e71..e6b4aed70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@stream-io/react-native-webrtc", - "version": "137.2.0-alpha.3", + "version": "137.2.0-alpha.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@stream-io/react-native-webrtc", - "version": "137.2.0-alpha.3", + "version": "137.2.0-alpha.5", "license": "MIT", "dependencies": { "base64-js": "1.5.1", diff --git a/package.json b/package.json index 36a9e1f6c..8dfd94b46 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@stream-io/react-native-webrtc", - "version": "137.2.0-alpha.3", + "version": "137.2.0-alpha.5", "repository": { "type": "git", "url": "git+https://github.com/GetStream/react-native-webrtc.git"