Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
* The implementation of {@code getUserMedia} extracted into a separate file in
* order to reduce complexity and to (somewhat) separate concerns.
*/
class GetUserMediaImpl {
public class GetUserMediaImpl {
/**
* The {@link Log} tag with which {@code GetUserMediaImpl} is to log.
*/
Expand All @@ -62,6 +62,15 @@ class GetUserMediaImpl {
private Promise displayMediaPromise;
private Intent mediaProjectionPermissionResultData;

/**
* Returns the MediaProjection permission result data Intent.
* This Intent can be used to create a MediaProjection for audio capture
* via AudioPlaybackCaptureConfiguration.
*/
public Intent getMediaProjectionPermissionResultData() {
return mediaProjectionPermissionResultData;
}

private final ServiceConnection mediaProjectionServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Expand Down Expand Up @@ -355,7 +364,9 @@ private void createScreenStream() {
}

// Cleanup
mediaProjectionPermissionResultData = null;
// Note: mediaProjectionPermissionResultData is intentionally NOT nulled here.
// It is retained so it can be reused to create a MediaProjection for
// screen share audio capture (AudioPlaybackCaptureConfiguration).
displayMediaPromise = null;
Comment on lines +367 to 370
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how/where the MediaProjection permission data is used for audio capture
# and whether there's lifecycle management

rg -n "getMediaProjectionPermissionResultData|AudioPlaybackCapture" --type java

Repository: GetStream/react-native-webrtc

Length of output: 462


🏁 Script executed:

# First, get the full GetUserMediaImpl.java file to understand the context
wc -l android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java

Repository: GetStream/react-native-webrtc

Length of output: 142


🏁 Script executed:

# Get the relevant section and understand how mediaProjectionPermissionResultData is stored and used
cat -n android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java | sed -n '60,80p'

Repository: GetStream/react-native-webrtc

Length of output: 1117


🏁 Script executed:

# Check if there's any cleanup/reset of mediaProjectionPermissionResultData
rg -n "mediaProjectionPermissionResultData\s*=" android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java

Repository: GetStream/react-native-webrtc

Length of output: 140


🏁 Script executed:

# Find all usages of getMediaProjectionPermissionResultData() across the codebase
rg -n "getMediaProjectionPermissionResultData\(\)" --type java -A 3 -B 1

Repository: GetStream/react-native-webrtc

Length of output: 545


🏁 Script executed:

# Check for any lifecycle-related cleanup (e.g., onDestroy, cleanup, clear methods)
rg -n "onDestroy|cleanup|clear.*Intent|reset.*MediaProjection" android/src/main/java/com/oney/WebRTCModule/ --type java

Repository: GetStream/react-native-webrtc

Length of output: 55


🏁 Script executed:

# Get more context around where mediaProjectionPermissionResultData is set (line 105)
cat -n android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java | sed -n '95,120p'

Repository: GetStream/react-native-webrtc

Length of output: 1315


🏁 Script executed:

# Check the entire createScreenStream method and related methods for cleanup
cat -n android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java | sed -n '350,380p'

Repository: GetStream/react-native-webrtc

Length of output: 1627


🏁 Script executed:

# Look for stop/end/cleanup methods related to screen sharing
rg -n "stopScreenCapture|endScreenShare|stopDisplayMedia" --type java

Repository: GetStream/react-native-webrtc

Length of output: 55


🏁 Script executed:

# Check where screen sharing ends and if there's any cleanup
rg -n "displayMediaPromise.*null|mediaProjection.*null|unbind.*Service" android/src/main/java/com/oney/WebRTCModule/ --type java -B 2 -A 2

Repository: GetStream/react-native-webrtc

Length of output: 3201


🏁 Script executed:

# Check how ScreenCaptureController uses the mediaProjectionPermissionResultData
fd -e java -x grep -l "ScreenCaptureController" {} \;

Repository: GetStream/react-native-webrtc

Length of output: 215


🏁 Script executed:

# Search for actual calls to getMediaProjectionPermissionResultData() outside of the class
rg -n "getMediaProjectionPermissionResultData" --type java -B 3 -A 3

Repository: GetStream/react-native-webrtc

Length of output: 806


🏁 Script executed:

# Check ScreenCaptureController.java to see if it uses mediaProjectionPermissionResultData
cat -n android/src/main/java/com/oney/WebRTCModule/ScreenCaptureController.java | head -100

Repository: GetStream/react-native-webrtc

Length of output: 3934


🏁 Script executed:

# Look for where ScreenCaptureController gets the Intent/data
rg -n "new ScreenCaptureController|getMediaProjectionPermissionResultData" android/src/main/java/com/oney/WebRTCModule/ --type java -B 2 -A 2

Repository: GetStream/react-native-webrtc

Length of output: 1290


🏁 Script executed:

# Check if there's any method to reset or clear the Intent when screen sharing stops
cat -n android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java | sed -n '1,50p'

Repository: GetStream/react-native-webrtc

Length of output: 2163


🏁 Script executed:

# Search for AudioPlaybackCaptureConfiguration usage in the codebase
rg -n "AudioPlaybackCaptureConfiguration" --type java -B 3 -A 3

Repository: GetStream/react-native-webrtc

Length of output: 1668


🏁 Script executed:

# Check if getMediaProjectionPermissionResultData is actually called from any native modules or bindings
rg -n "getMediaProjectionPermissionResultData" . --type java --type js --type ts -B 2 -A 2

Repository: GetStream/react-native-webrtc

Length of output: 600


🏁 Script executed:

# Look for any MediaProjection creation or reuse patterns
rg -n "createMediaProjection|getMediaProjection" --type java -B 2 -A 2

Repository: GetStream/react-native-webrtc

Length of output: 590


🏁 Script executed:

# Check if there's any lifecycle management or cleanup when screen sharing ends
cat -n android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java | sed -n '400,450p'

Repository: GetStream/react-native-webrtc

Length of output: 2582


🏁 Script executed:

# Check if there are any issues or tests that mention stale Intent or MediaProjection problems
rg -n "stale|lifecycle|MediaProjection.*reuse|AudioPlaybackCapture" --type java -B 2 -A 2

Repository: GetStream/react-native-webrtc

Length of output: 1310


🏁 Script executed:

# Check the complete flow of when/how getMediaProjectionPermissionResultData is intended to be called
rg -n "public|protected" android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java | head -30

Repository: GetStream/react-native-webrtc

Length of output: 1128


🏁 Script executed:

# Look for any documentation or comments about the intended use of getMediaProjectionPermissionResultData()
cat -n android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java | sed -n '65,112p'

Repository: GetStream/react-native-webrtc

Length of output: 2344


🏁 Script executed:

# Check if ScreenCapturerAndroid or MediaProjection has any caching/reuse pattern that would support audio capture
rg -n "ScreenCapturerAndroid|createMediaProjection" . --type java -B 3 -A 3 | head -60

Repository: GetStream/react-native-webrtc

Length of output: 1744


🏁 Script executed:

# Check if there are any JavaScript/TypeScript files that might use getMediaProjectionPermissionResultData
find . -type f \( -name "*.js" -o -name "*.ts" -o -name "*.tsx" \) -exec grep -l "getMediaProjectionPermissionResultData\|AudioPlaybackCapture" {} \;

Repository: GetStream/react-native-webrtc

Length of output: 55


🏁 Script executed:

# Check if MediaProjectionService has any cleanup or lifecycle management
cat -n android/src/main/java/com/oney/WebRTCModule/MediaProjectionService.java 2>/dev/null | head -100

Repository: GetStream/react-native-webrtc

Length of output: 4173


🏁 Script executed:

# Find the MediaProjectionService file
find . -name "MediaProjectionService.java" -type f

Repository: GetStream/react-native-webrtc

Length of output: 146


Add lifecycle management to clear the retained Intent when screen sharing ends.

The mediaProjectionPermissionResultData Intent is retained for reuse in AudioPlaybackCaptureConfiguration, but there's no mechanism to clear it when:

  • The MediaProjection is revoked (e.g., user stops screen sharing)
  • The ScreenCapturerAndroid.onStop() callback fires

Since getMediaProjectionPermissionResultData() is a public API, external consumers could retrieve and use a stale Intent for audio capture, which would fail. Either clear the Intent explicitly when the MediaProjection ends, or add validation to ensure freshness before use.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java` around
lines 367 - 370, Retainment of mediaProjectionPermissionResultData can leak a
stale Intent; update GetUserMediaImpl to clear
mediaProjectionPermissionResultData when the MediaProjection is revoked or when
ScreenCapturerAndroid.onStop() is called and also null out displayMediaPromise
there; implement this by hooking into the MediaProjection callback/stop path
(where the MediaProjection is released) and calling
mediaProjectionPermissionResultData = null (and displayMediaPromise = null if
applicable), and add a freshness check inside
getMediaProjectionPermissionResultData() to validate the Intent (e.g., ensure a
live MediaProjection or timestamp/flag) and return null or throw if it is stale
so external callers cannot reuse an invalid Intent.

}

Expand Down
57 changes: 57 additions & 0 deletions android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,62 @@ private JavaAudioDeviceModule createAudioDeviceModule(ReactApplicationContext re
.setUseHardwareAcousticEchoCanceler(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
.setUseHardwareNoiseSuppressor(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
.setUseStereoOutput(true)
.setAudioBufferCallback((audioBuffer, audioFormat, channelCount, sampleRate, bytesRead, captureTimeNs) -> {
if (bytesRead > 0) {
WebRTCModuleOptions.ScreenAudioBytesProvider provider =
WebRTCModuleOptions.getInstance().screenAudioBytesProvider;
if (provider != null) {
java.nio.ByteBuffer screenBuffer = provider.getScreenAudioBytes(bytesRead);
if (screenBuffer != null && screenBuffer.remaining() > 0) {
mixScreenAudioIntoBuffer(audioBuffer, screenBuffer, bytesRead);
}
}
}
return captureTimeNs;
})
.createAudioDeviceModule();
}

/**
* Mixes screen audio into the microphone buffer using PCM 16-bit additive mixing
* with clamping. Handles different buffer sizes safely: each buffer is read only
* within its own bounds. When one buffer is shorter, the other's samples pass
* through unmodified (mic samples stay as-is, or screen-only samples are written).
*/
private static void mixScreenAudioIntoBuffer(java.nio.ByteBuffer micBuffer,
java.nio.ByteBuffer screenBuffer,
int bytesRead) {
micBuffer.position(0);
screenBuffer.position(0);

micBuffer.order(java.nio.ByteOrder.LITTLE_ENDIAN);
screenBuffer.order(java.nio.ByteOrder.LITTLE_ENDIAN);

java.nio.ShortBuffer micShorts = micBuffer.asShortBuffer();
java.nio.ShortBuffer screenShorts = screenBuffer.asShortBuffer();

int micSamples = Math.min(bytesRead / 2, micShorts.remaining());
int screenSamples = screenShorts.remaining();
int totalSamples = Math.max(micSamples, screenSamples);

for (int i = 0; i < totalSamples; i++) {
int sum;
if (i >= micSamples) {
// Screen-only: mic buffer is shorter — write screen sample directly
sum = screenShorts.get(i);
} else if (i >= screenSamples) {
// Mic-only: screen buffer is shorter — keep mic sample as-is
break;
} else {
// Both buffers have data — add samples
sum = micShorts.get(i) + screenShorts.get(i);
}
if (sum > Short.MAX_VALUE) sum = Short.MAX_VALUE;
if (sum < Short.MIN_VALUE) sum = Short.MIN_VALUE;
micShorts.put(i, (short) sum);
}
}

@NonNull
@Override
public String getName() {
Expand All @@ -142,6 +195,10 @@ public AudioDeviceModule getAudioDeviceModule() {
return mAudioDeviceModule;
}

public GetUserMediaImpl getUserMediaImpl() {
return getUserMediaImpl;
}

public PeerConnectionObserver getPeerConnectionObserver(int id) {
return mPeerConnectionObservers.get(id);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import org.webrtc.VideoEncoderFactory;
import org.webrtc.audio.AudioDeviceModule;

import java.nio.ByteBuffer;

public class WebRTCModuleOptions {
private static WebRTCModuleOptions instance;

Expand All @@ -20,6 +22,29 @@ public class WebRTCModuleOptions {
public boolean enableMediaProjectionService;
public AudioProcessingFactoryProvider audioProcessingFactoryProvider;

/**
* Provider for screen share audio bytes. When set, the AudioDeviceModule's
* AudioBufferCallback will mix screen audio into the mic buffer before
* WebRTC processing. This allows screen audio mixing to work alongside
* any audio processing factory (including noise cancellation).
*
* Set this when screen share audio capture starts, clear it when it stops.
*/
public volatile ScreenAudioBytesProvider screenAudioBytesProvider;

/**
* Functional interface for providing screen audio bytes on demand.
*/
public interface ScreenAudioBytesProvider {
/**
* Returns a ByteBuffer containing screen audio PCM data.
*
* @param bytesRequested number of bytes to read (matching mic buffer size)
* @return ByteBuffer with screen audio, or null if not available
*/
ByteBuffer getScreenAudioBytes(int bytesRequested);
}

public static WebRTCModuleOptions getInstance() {
if (instance == null) {
instance = new WebRTCModuleOptions();
Expand Down
18 changes: 18 additions & 0 deletions ios/RCTWebRTC/InAppScreenCaptureController.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#import <Foundation/Foundation.h>
#import "CaptureController.h"
#import "CapturerEventsDelegate.h"

NS_ASSUME_NONNULL_BEGIN

@class InAppScreenCapturer;

@interface InAppScreenCaptureController : CaptureController

- (instancetype)initWithCapturer:(nonnull InAppScreenCapturer *)capturer;

/// The underlying RPScreenRecorder-based capturer.
@property(nonatomic, strong, readonly) InAppScreenCapturer *capturer;

@end

NS_ASSUME_NONNULL_END
49 changes: 49 additions & 0 deletions ios/RCTWebRTC/InAppScreenCaptureController.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#if TARGET_OS_IOS

#import "InAppScreenCaptureController.h"
#import "InAppScreenCapturer.h"

@interface InAppScreenCaptureController () <CapturerEventsDelegate>
@end

@implementation InAppScreenCaptureController

- (instancetype)initWithCapturer:(nonnull InAppScreenCapturer *)capturer {
self = [super init];
if (self) {
_capturer = capturer;
_capturer.eventsDelegate = self;
self.deviceId = @"in-app-screen-capture";
}
return self;
}

- (void)dealloc {
[self.capturer stopCapture];
}

- (void)startCapture {
[self.capturer startCapture];
}

- (void)stopCapture {
[self.capturer stopCapture];
}

- (NSDictionary *)getSettings {
return @{
@"deviceId": self.deviceId ?: @"in-app-screen-capture",
@"groupId": @"",
@"frameRate": @(30)
};
}

#pragma mark - CapturerEventsDelegate

- (void)capturerDidEnd:(RTCVideoCapturer *)capturer {
[self.eventsDelegate capturerDidEnd:capturer];
}

@end

#endif
21 changes: 21 additions & 0 deletions ios/RCTWebRTC/InAppScreenCapturer.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#import <AVFoundation/AVFoundation.h>
#import <WebRTC/RTCVideoCapturer.h>
#import "CapturerEventsDelegate.h"

NS_ASSUME_NONNULL_BEGIN

@interface InAppScreenCapturer : RTCVideoCapturer

@property(nonatomic, weak) id<CapturerEventsDelegate> eventsDelegate;

/// Callback invoked for each .audioApp CMSampleBuffer from RPScreenRecorder.
/// Set this before calling startCapture if audio mixing is desired.
@property(nonatomic, copy, nullable) void (^audioBufferHandler)(CMSampleBufferRef);

- (instancetype)initWithDelegate:(__weak id<RTCVideoCapturerDelegate>)delegate;
- (void)startCapture;
- (void)stopCapture;

@end

NS_ASSUME_NONNULL_END
Loading
Loading