3636import io .getstream .webrtc .flutter .audio .AudioSwitchManager ;
3737import io .getstream .webrtc .flutter .audio .AudioUtils ;
3838import io .getstream .webrtc .flutter .audio .LocalAudioTrack ;
39+ import io .getstream .webrtc .flutter .audio .ScreenAudioCapturer ;
40+
41+ import java .nio .ByteBuffer ;
3942import io .getstream .webrtc .flutter .record .AudioChannel ;
4043import io .getstream .webrtc .flutter .record .AudioSamplesInterceptor ;
4144import io .getstream .webrtc .flutter .record .MediaRecorderImpl ;
@@ -127,6 +130,9 @@ public class GetUserMediaImpl {
127130 private boolean isTorchOn ;
128131 private Intent mediaProjectionData = null ;
129132
133+ private ScreenAudioCapturer screenAudioCapturer ;
134+ private volatile boolean screenAudioEnabled = false ;
135+ private OrientationAwareScreenCapturer currentScreenCapturer ;
130136
131137 public void screenRequestPermissions (ResultReceiver resultReceiver ) {
132138 mediaProjectionData = null ;
@@ -176,15 +182,40 @@ public static class ScreenRequestPermissionsFragment extends Fragment {
176182
177183 private ResultReceiver resultReceiver = null ;
178184 private int requestCode = 0 ;
179- private final int resultCode = 0 ;
185+ private int resultCode = 0 ;
186+ private boolean hasRequestedPermission = false ;
180187
181188 private void checkSelfPermissions (boolean requestPermissions ) {
189+ // Avoid requesting permission multiple times
190+ if (hasRequestedPermission ) {
191+ return ;
192+ }
193+
182194 if (resultCode != Activity .RESULT_OK ) {
183195 Activity activity = this .getActivity ();
196+ if (activity == null || activity .isFinishing ()) {
197+ return ;
198+ }
199+
184200 Bundle args = getArguments ();
201+ if (args == null ) {
202+ return ;
203+ }
204+
185205 resultReceiver = args .getParcelable (RESULT_RECEIVER );
186206 requestCode = args .getInt (REQUEST_CODE );
187- requestStart (activity , requestCode );
207+
208+ hasRequestedPermission = true ;
209+
210+ // Post the permission request to allow the activity to fully stabilize.
211+ // This helps prevent the app from going to background on Samsung and other
212+ // devices when the MediaProjection permission dialog appears.
213+ new Handler (Looper .getMainLooper ()).postDelayed (() -> {
214+ Activity currentActivity = getActivity ();
215+ if (currentActivity != null && !currentActivity .isFinishing () && isAdded ()) {
216+ requestStart (currentActivity , requestCode );
217+ }
218+ }, 100 );
188219 }
189220 }
190221
@@ -495,6 +526,9 @@ public void invoke(Object... args) {
495526
496527 void getDisplayMedia (
497528 final ConstraintsMap constraints , final Result result , final MediaStream mediaStream ) {
529+ // Check if audio is requested for screen share
530+ final boolean includeAudio = parseIncludeAudio (constraints );
531+
498532 if (mediaProjectionData == null ) {
499533 screenRequestPermissions (
500534 new ResultReceiver (new Handler (Looper .getMainLooper ())) {
@@ -507,41 +541,76 @@ protected void onReceiveResult(int requestCode, Bundle resultData) {
507541 resultError ("screenRequestPermissions" , "User didn't give permission to capture the screen." , result );
508542 return ;
509543 }
510- getDisplayMedia (result , mediaStream , mediaProjectionData );
544+ getDisplayMedia (result , mediaStream , mediaProjectionData , includeAudio );
511545 }
512546 });
513547 } else {
514- getDisplayMedia (result , mediaStream , mediaProjectionData );
548+ getDisplayMedia (result , mediaStream , mediaProjectionData , includeAudio );
515549 }
516550 }
517551
518- private void getDisplayMedia (final Result result , final MediaStream mediaStream , final Intent mediaProjectionData ) {
552+ /**
553+ * Parses the includeAudio flag from constraints.
554+ * Checks for audio: true or audio: { ... } in constraints.
555+ */
556+ private boolean parseIncludeAudio (ConstraintsMap constraints ) {
557+ if (constraints == null || !constraints .hasKey ("audio" )) {
558+ return false ;
559+ }
560+
561+ ObjectType audioType = constraints .getType ("audio" );
562+ if (audioType == ObjectType .Boolean ) {
563+ return constraints .getBoolean ("audio" );
564+ } else if (audioType == ObjectType .Map ) {
565+ // If audio is a map/object, we treat it as audio enabled
566+ return true ;
567+ }
568+
569+ return false ;
570+ }
571+
572+ private void getDisplayMedia (final Result result , final MediaStream mediaStream ,
573+ final Intent mediaProjectionData , final boolean includeAudio ) {
519574 /* Create ScreenCapture */
520575 VideoTrack displayTrack = null ;
521- VideoCapturer videoCapturer = null ;
522576 String trackId = stateProvider .getNextTrackUUID ();
523577
524- videoCapturer = new OrientationAwareScreenCapturer (
578+ OrientationAwareScreenCapturer videoCapturer = new OrientationAwareScreenCapturer (
525579 applicationContext ,
526580 mediaProjectionData ,
527581 new MediaProjection .Callback () {
528582 @ Override
529583 public void onStop () {
530584 super .onStop ();
531585
586+ // Stop screen audio capture when screen sharing stops
587+ stopScreenAudioCapture ();
588+
532589 ConstraintsMap params = new ConstraintsMap ();
533590 params .putString ("event" , EVENT_DISPLAY_MEDIA_STOPPED );
534591 params .putString ("trackId" , trackId );
535592 FlutterWebRTCPlugin .sharedSingleton .sendEvent (params .toMap ());
536593 }
537- }
538- );
594+ });
539595
540- if (videoCapturer == null ) {
541- resultError ("screenRequestPermissions" , "GetDisplayMediaFailed, User revoked permission to capture the screen." , result );
542- return ;
596+ // Set up screen audio capture listener if audio is requested
597+ if (includeAudio && ScreenAudioCapturer .isSupported ()) {
598+ videoCapturer .setMediaProjectionReadyListener (
599+ new OrientationAwareScreenCapturer .MediaProjectionReadyListener () {
600+ @ Override
601+ public void onMediaProjectionReady (MediaProjection mediaProjection ) {
602+ startScreenAudioCapture (mediaProjection );
603+ }
604+
605+ @ Override
606+ public void onMediaProjectionStopped () {
607+ stopScreenAudioCapture ();
608+ }
609+ });
543610 }
544611
612+ currentScreenCapturer = videoCapturer ;
613+
545614 PeerConnectionFactory pcFactory = stateProvider .getPeerConnectionFactory ();
546615 VideoSource videoSource = pcFactory .createVideoSource (true );
547616
@@ -566,7 +635,8 @@ public void onStop() {
566635 info .capturer = videoCapturer ;
567636
568637 videoCapturer .startCapture (info .width , info .height , info .fps );
569- Log .d (TAG , "OrientationAwareScreenCapturer.startCapture: " + info .width + "x" + info .height + "@" + info .fps );
638+ Log .d (TAG , "OrientationAwareScreenCapturer.startCapture: " + info .width + "x" + info .height + "@" + info .fps +
639+ ", includeAudio: " + includeAudio );
570640
571641 mVideoCapturers .put (trackId , info );
572642 mVideoSources .put (trackId , videoSource );
@@ -609,6 +679,56 @@ public void onStop() {
609679 result .success (successResult .toMap ());
610680 }
611681
682+ @ RequiresApi (api = Build .VERSION_CODES .Q )
683+ private void startScreenAudioCapture (MediaProjection mediaProjection ) {
684+ if (!ScreenAudioCapturer .isSupported ()) {
685+ Log .w (TAG , "Screen audio capture not supported on this device" );
686+ return ;
687+ }
688+
689+ if (screenAudioCapturer == null ) {
690+ screenAudioCapturer = new ScreenAudioCapturer (applicationContext );
691+ }
692+
693+ boolean started = screenAudioCapturer .startCapture (mediaProjection );
694+ screenAudioEnabled = started ;
695+
696+ if (started ) {
697+ Log .d (TAG , "Screen audio capture started successfully" );
698+ } else {
699+ Log .w (TAG , "Failed to start screen audio capture" );
700+ }
701+ }
702+
703+ private synchronized void stopScreenAudioCapture () {
704+ screenAudioEnabled = false ;
705+
706+ ScreenAudioCapturer localCapturer = screenAudioCapturer ;
707+ if (localCapturer != null ) {
708+ localCapturer .stopCapture ();
709+ Log .d (TAG , "Screen audio capture stopped" );
710+ }
711+ }
712+
713+ /**
714+ * Returns whether screen audio capture is currently enabled.
715+ */
716+ public boolean isScreenAudioEnabled () {
717+ return screenAudioEnabled && screenAudioCapturer != null && screenAudioCapturer .isCapturing ();
718+ }
719+
720+ /**
721+ * Gets screen audio bytes for mixing with microphone audio.
722+ * Returns null if screen audio capture is not active.
723+ */
724+ public ByteBuffer getScreenAudioBytes (int bytesRequested ) {
725+ if (!isScreenAudioEnabled ()) {
726+ return null ;
727+ }
728+
729+ return screenAudioCapturer .getScreenAudioBytes (bytesRequested );
730+ }
731+
612732 /**
613733 * Implements {@code getUserMedia} with the knowledge that the necessary permissions have already
614734 * been granted. If the necessary permissions have not been granted yet, they will NOT be
0 commit comments