@@ -65,6 +65,13 @@ class GetUserMediaImpl {
6565 private boolean createConfigForDefaultDisplay = false ;
6666 private float resolutionScale = 1.0f ;
6767
68+ // Reusable SurfaceTextureHelper for camera captures.
69+ // By reusing a single STH for all camera sessions we hold exactly one EGL context
70+ // regardless of how many times the camera is toggled. On dispose we only call
71+ // stopListening() so
72+ // the context stays alive for the next getUserMedia call.
73+ private SurfaceTextureHelper reusableCameraSTH = null ;
74+
6875 GetUserMediaImpl (WebRTCModule webRTCModule , ReactApplicationContext reactContext ) {
6976 this .webRTCModule = webRTCModule ;
7077 this .reactContext = reactContext ;
@@ -439,7 +446,18 @@ VideoTrack createVideoTrack(AbstractVideoCaptureController videoCaptureControlle
439446
440447 PeerConnectionFactory pcFactory = webRTCModule .mFactory ;
441448 EglBase .Context eglContext = EglUtils .getRootEglBaseContext ();
442- SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper .create ("CaptureThread" , eglContext );
449+
450+ boolean isCameraCapture = videoCaptureController instanceof CameraCaptureController ;
451+ SurfaceTextureHelper surfaceTextureHelper ;
452+
453+ if (isCameraCapture ) {
454+ if (reusableCameraSTH == null ) {
455+ reusableCameraSTH = SurfaceTextureHelper .create ("CaptureThread" , eglContext );
456+ }
457+ surfaceTextureHelper = reusableCameraSTH ;
458+ } else {
459+ surfaceTextureHelper = SurfaceTextureHelper .create ("CaptureThread" , eglContext );
460+ }
443461
444462 if (surfaceTextureHelper == null ) {
445463 Log .d (TAG , "Error creating SurfaceTextureHelper" );
@@ -457,7 +475,8 @@ VideoTrack createVideoTrack(AbstractVideoCaptureController videoCaptureControlle
457475 VideoTrack track = pcFactory .createVideoTrack (id , videoSource );
458476
459477 track .setEnabled (true );
460- tracks .put (id , new TrackPrivate (track , videoSource , videoCaptureController , surfaceTextureHelper ));
478+ tracks .put (id ,
479+ new TrackPrivate (track , videoSource , videoCaptureController , surfaceTextureHelper , isCameraCapture ));
461480
462481 videoCaptureController .startCapture ();
463482
@@ -523,26 +542,32 @@ private static class TrackPrivate {
523542 private final SurfaceTextureHelper surfaceTextureHelper ;
524543
525544 /**
526- * Whether this object has been disposed or not.
545+ * When true, {@link #surfaceTextureHelper} is the shared reusable camera STH owned by
546+ * {@link GetUserMediaImpl#reusableCameraSTH}. Its EGL context must NOT be destroyed on
547+ * dispose — only {@code stopListening()} is called so the context can be reclaimed by the
548+ * next camera session. Non-camera STHs (screen share, etc.) are not reused and are fully
549+ * disposed.
527550 */
528- private boolean disposed ;
551+ private final boolean reusableSTH ;
529552
530553 /**
531- * Initializes a new {@code TrackPrivate} instance.
532- *
533- * @param track
534- * @param mediaSource the {@code MediaSource} from which the specified
535- * {@code code} was created
536- * @param videoCaptureController the {@code AbstractVideoCaptureController} from which the
537- * specified {@code mediaSource} was created if the specified
538- * {@code track} is a {@link VideoTrack}
554+ * Whether this object has been disposed or not.
539555 */
556+ private boolean disposed ;
557+
540558 public TrackPrivate (MediaStreamTrack track , MediaSource mediaSource ,
541559 AbstractVideoCaptureController videoCaptureController , SurfaceTextureHelper surfaceTextureHelper ) {
560+ this (track , mediaSource , videoCaptureController , surfaceTextureHelper , false );
561+ }
562+
563+ public TrackPrivate (MediaStreamTrack track , MediaSource mediaSource ,
564+ AbstractVideoCaptureController videoCaptureController , SurfaceTextureHelper surfaceTextureHelper ,
565+ boolean reusableSTH ) {
542566 this .track = track ;
543567 this .mediaSource = mediaSource ;
544568 this .videoCaptureController = videoCaptureController ;
545569 this .surfaceTextureHelper = surfaceTextureHelper ;
570+ this .reusableSTH = reusableSTH ;
546571 this .disposed = false ;
547572 }
548573
@@ -558,11 +583,17 @@ public void dispose() {
558583 * As per webrtc library documentation - The caller still has ownership of {@code
559584 * surfaceTextureHelper} and is responsible for making sure surfaceTextureHelper.dispose() is
560585 * called. This also means that the caller can reuse the SurfaceTextureHelper to initialize a new
561- * VideoCapturer once the previous VideoCapturer has been disposed. */
562-
586+ * VideoCapturer once the previous VideoCapturer has been disposed.
587+ *
588+ * For camera captures we use a single reusable STH (GetUserMediaImpl.reusableCameraSTH)
589+ * so we only call stopListening() here — the EGL context is kept alive for the next
590+ * getUserMedia call. Full dispose() is only called for non-reusable STHs (screen share).
591+ */
563592 if (surfaceTextureHelper != null ) {
564593 surfaceTextureHelper .stopListening ();
565- surfaceTextureHelper .dispose ();
594+ if (!reusableSTH ) {
595+ surfaceTextureHelper .dispose ();
596+ }
566597 }
567598
568599 mediaSource .dispose ();
@@ -572,6 +603,28 @@ public void dispose() {
572603 }
573604 }
574605
606+ /**
607+ * Releases resources held by this instance. Must be called when the owning
608+ * {@link WebRTCModule} is invalidated (e.g. React Native JS reload) so that
609+ * the reusable camera EGL context and its GL thread are not kept alive for
610+ * the rest of the process lifetime.
611+ *
612+ * <p>All active {@link TrackPrivate} entries are disposed first so no capturer
613+ * is left running against an already-disposed STH.
614+ */
615+ void dispose () {
616+ for (TrackPrivate track : tracks .values ()) {
617+ track .dispose ();
618+ }
619+ tracks .clear ();
620+
621+ if (reusableCameraSTH != null ) {
622+ reusableCameraSTH .stopListening ();
623+ reusableCameraSTH .dispose ();
624+ reusableCameraSTH = null ;
625+ }
626+ }
627+
575628 public interface BiConsumer <T , U > {
576629 void accept (T t , U u );
577630 }
0 commit comments