Skip to content
Merged
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
83 changes: 68 additions & 15 deletions android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ class GetUserMediaImpl {
private boolean createConfigForDefaultDisplay = false;
private float resolutionScale = 1.0f;

// Reusable SurfaceTextureHelper for camera captures.
// By reusing a single STH for all camera sessions we hold exactly one EGL context
// regardless of how many times the camera is toggled. On dispose we only call
// stopListening() so
// the context stays alive for the next getUserMedia call.
private SurfaceTextureHelper reusableCameraSTH = null;
Comment thread
Magmusacy marked this conversation as resolved.

GetUserMediaImpl(WebRTCModule webRTCModule, ReactApplicationContext reactContext) {
this.webRTCModule = webRTCModule;
this.reactContext = reactContext;
Expand Down Expand Up @@ -439,7 +446,18 @@ VideoTrack createVideoTrack(AbstractVideoCaptureController videoCaptureControlle

PeerConnectionFactory pcFactory = webRTCModule.mFactory;
EglBase.Context eglContext = EglUtils.getRootEglBaseContext();
SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", eglContext);

boolean isCameraCapture = videoCaptureController instanceof CameraCaptureController;
SurfaceTextureHelper surfaceTextureHelper;

if (isCameraCapture) {
if (reusableCameraSTH == null) {
reusableCameraSTH = SurfaceTextureHelper.create("CaptureThread", eglContext);
}
surfaceTextureHelper = reusableCameraSTH;
} else {
surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", eglContext);
}

if (surfaceTextureHelper == null) {
Log.d(TAG, "Error creating SurfaceTextureHelper");
Expand All @@ -457,7 +475,8 @@ VideoTrack createVideoTrack(AbstractVideoCaptureController videoCaptureControlle
VideoTrack track = pcFactory.createVideoTrack(id, videoSource);

track.setEnabled(true);
tracks.put(id, new TrackPrivate(track, videoSource, videoCaptureController, surfaceTextureHelper));
tracks.put(id,
new TrackPrivate(track, videoSource, videoCaptureController, surfaceTextureHelper, isCameraCapture));

videoCaptureController.startCapture();

Expand Down Expand Up @@ -523,26 +542,32 @@ private static class TrackPrivate {
private final SurfaceTextureHelper surfaceTextureHelper;

/**
* Whether this object has been disposed or not.
* When true, {@link #surfaceTextureHelper} is the shared reusable camera STH owned by
* {@link GetUserMediaImpl#reusableCameraSTH}. Its EGL context must NOT be destroyed on
* dispose — only {@code stopListening()} is called so the context can be reclaimed by the
* next camera session. Non-camera STHs (screen share, etc.) are not reused and are fully
* disposed.
*/
private boolean disposed;
private final boolean reusableSTH;

/**
* Initializes a new {@code TrackPrivate} instance.
*
* @param track
* @param mediaSource the {@code MediaSource} from which the specified
* {@code code} was created
* @param videoCaptureController the {@code AbstractVideoCaptureController} from which the
* specified {@code mediaSource} was created if the specified
* {@code track} is a {@link VideoTrack}
* Whether this object has been disposed or not.
*/
private boolean disposed;

public TrackPrivate(MediaStreamTrack track, MediaSource mediaSource,
AbstractVideoCaptureController videoCaptureController, SurfaceTextureHelper surfaceTextureHelper) {
this(track, mediaSource, videoCaptureController, surfaceTextureHelper, false);
}

public TrackPrivate(MediaStreamTrack track, MediaSource mediaSource,
AbstractVideoCaptureController videoCaptureController, SurfaceTextureHelper surfaceTextureHelper,
boolean reusableSTH) {
this.track = track;
this.mediaSource = mediaSource;
this.videoCaptureController = videoCaptureController;
this.surfaceTextureHelper = surfaceTextureHelper;
this.reusableSTH = reusableSTH;
this.disposed = false;
}

Expand All @@ -558,11 +583,17 @@ public void dispose() {
* As per webrtc library documentation - The caller still has ownership of {@code
* surfaceTextureHelper} and is responsible for making sure surfaceTextureHelper.dispose() is
* called. This also means that the caller can reuse the SurfaceTextureHelper to initialize a new
* VideoCapturer once the previous VideoCapturer has been disposed. */

* VideoCapturer once the previous VideoCapturer has been disposed.
*
* For camera captures we use a single reusable STH (GetUserMediaImpl.reusableCameraSTH)
* so we only call stopListening() here — the EGL context is kept alive for the next
* getUserMedia call. Full dispose() is only called for non-reusable STHs (screen share).
*/
if (surfaceTextureHelper != null) {
surfaceTextureHelper.stopListening();
surfaceTextureHelper.dispose();
if (!reusableSTH) {
surfaceTextureHelper.dispose();
}
}
Comment thread
Magmusacy marked this conversation as resolved.

mediaSource.dispose();
Expand All @@ -572,6 +603,28 @@ public void dispose() {
}
}

/**
* Releases resources held by this instance. Must be called when the owning
* {@link WebRTCModule} is invalidated (e.g. React Native JS reload) so that
* the reusable camera EGL context and its GL thread are not kept alive for
* the rest of the process lifetime.
*
* <p>All active {@link TrackPrivate} entries are disposed first so no capturer
* is left running against an already-disposed STH.
*/
void dispose() {
for (TrackPrivate track : tracks.values()) {
track.dispose();
}
tracks.clear();

if (reusableCameraSTH != null) {
reusableCameraSTH.stopListening();
reusableCameraSTH.dispose();
reusableCameraSTH = null;
}
}
Comment thread
Magmusacy marked this conversation as resolved.

public interface BiConsumer<T, U> {
void accept(T t, U u);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ public void initialize() {
public void invalidate() {
super.invalidate();
audioOutputManager.stopObserving();
getUserMediaImpl.dispose();
}

@NonNull
Expand Down
90 changes: 64 additions & 26 deletions android/src/main/java/com/oney/WebRTCModule/WebRTCView.java
Original file line number Diff line number Diff line change
Expand Up @@ -619,50 +619,88 @@ void setStreamURL(String streamURL) {
/**
* Sets the {@code VideoTrack} to be rendered by this {@code WebRTCView}.
*
* <p>The SurfaceViewRenderer (and its underlying EGL context) is kept alive as long as this
* view is attached to a window, even when the track becomes {@code null} (camera off). This
* avoids the pattern of calling {@code SurfaceViewRenderer.release()} on camera-off and
* {@code SurfaceViewRenderer.init()} on camera-on, which each time allocates a new EGL context
* from Android's finite per-process pool and can produce:
* {@code android.opengl.GLException: Failed to create EGL context: 0x3000}
* {@code release()} is only called from {@link #onDetachedFromWindow()}.
*
* @param videoTrack The {@code VideoTrack} to be rendered by this
* {@code WebRTCView} or {@code null}.
*/
private void setVideoTrack(VideoTrack videoTrack) {
VideoTrack oldVideoTrack = this.videoTrack;

if (oldVideoTrack != videoTrack) {
if (oldVideoTrack != null && videoTrack != null && rendererAttached) {
final VideoTrack capturedOld = oldVideoTrack;
if (oldVideoTrack == videoTrack) {
return;
}

// Case 1: Swapping one live track for another while the renderer is active.
// Just move the sink — no EGL context churn.
if (oldVideoTrack != null && videoTrack != null && rendererAttached) {
final VideoTrack capturedOld = oldVideoTrack;
final VideoTrack capturedNew = videoTrack;
this.videoTrack = videoTrack;
ThreadUtils.runOnExecutor(() -> {
try {
capturedOld.removeSink(surfaceViewRenderer);
} catch (Throwable tr) {
Log.w(TAG, "Failed to remove sink from old track", tr);
}
if (!rendererAttached) {
return;
}
try {
capturedNew.addSink(surfaceViewRenderer);
} catch (Throwable tr) {
Log.e(TAG, "Failed to add renderer", tr);
}
});
return;
}

// Detach the old track's sink from the renderer (without releasing the EGL context).
if (oldVideoTrack != null && rendererAttached) {
final VideoTrack trackToRemove = oldVideoTrack;
ThreadUtils.runOnExecutor(() -> {
try {
trackToRemove.removeSink(surfaceViewRenderer);
} catch (Throwable tr) {
Log.w(TAG, "Failed to remove sink from track", tr);
}
});
}

this.videoTrack = videoTrack;

if (videoTrack != null) {
if (rendererAttached) {
// The EGL context was kept alive from a previous session — just add the sink.
if (oldVideoTrack == null) {
cleanSurfaceViewRenderer();
}
final VideoTrack capturedNew = videoTrack;
this.videoTrack = videoTrack;
ThreadUtils.runOnExecutor(() -> {
try {
capturedOld.removeSink(surfaceViewRenderer);
} catch (Throwable tr) {
Log.w(TAG, "Failed to remove sink from old track", tr);
}
if (!rendererAttached) {
return;
}
try {
capturedNew.addSink(surfaceViewRenderer);
} catch (Throwable tr) {
Log.e(TAG, "Failed to add renderer", tr);
}
});
return;
}

if (oldVideoTrack != null) {
if (videoTrack == null) {
cleanSurfaceViewRenderer();
}
removeRendererFromVideoTrack();
}

this.videoTrack = videoTrack;

if (videoTrack != null) {
tryAddRendererToVideoTrack();
} else {
// Renderer not yet initialised (first render after attach, or after detach).
if (oldVideoTrack == null) {
cleanSurfaceViewRenderer();
}
tryAddRendererToVideoTrack();
}
} else {
// Track became null (camera off). Keep the EGL context alive so the next
// camera-on can reuse it without calling surfaceViewRenderer.init() again.
// The renderer is only released in onDetachedFromWindow().
cleanSurfaceViewRenderer();
}
}

Expand Down
Loading