Skip to Content
InternalsScreen sharing

Screen sharing

Screen sharing is MediaProjectionScreenCapturerAndroid → the outgoing video track, plus device-playback audio mixed into the mic stream, plus orientation follow-through.

Starting a share

The resolution presets map to concrete capture requests and bitrate caps (see ScreenShareResolution). “Native” shares the full panel at 60 fps.

The capture pipeline

  • Dedicated SurfaceTextureHelper ("ScreenCaptureThread"). Sharing the camera capturer’s helper leaves the MediaProjection virtual display with an invalid producer surface (SurfaceFlinger: dequeueBuffer failed … error: -19) and zero frames are ever delivered.
  • The camera capturer is stopped first; the screen capturer feeds the same VideoSource via its capturerObserver.
  • degradationPreference = MAINTAIN_FRAMERATE. MAINTAIN_RESOLUTION pinned the resolution and let the frame rate collapse to ~2 fps on weaker H.265 encoders. Screen content stays legible when WebRTC sheds resolution under pressure and recovers when it eases; a 2 fps slideshow does not.
  • applyScreenShareBitrate() - the camera preset’s bitrate cap is far too low for 1080p/native screen content, so the cap is raised to the preset’s value and scaleResolutionDownBy cleared.

Screencast source - pending

Ideally the screen share would run on its own createVideoSource(isScreencast = true) rather than reusing the camera source. That source pins resolution and lets FPS give - the correct trade-off for a screen, and the opposite of the camera source. That change plus a native-resolution bitrate bump is a pending improvement.

Device audio

ScreenAudioCapturer reuses the same MediaProjection to capture device playback (AudioPlaybackCaptureConfiguration) and mixes it into the outgoing mic buffer as the last stage of CompositeAudioProcessing - after RNNoise and gain, so the shared media audio isn’t denoised or re-gained. Stops with the share.

Orientation follow-through

ScreenCapturerAndroid never resizes its virtual display on its own, so rotating the sharer’s phone just letterboxes the new orientation into the old buffer.

MainActivity watches for rotation (a DisplayListener + onConfigurationChanged) and calls WebRtcClient.resizeScreenShare(w, h), which changeCaptureFormats the capturer to the swapped dimensions. The new rotation is also sent to the peer on the screen-share frame (rotation field) so the viewer’s PiP window can match aspect ratio, and - if screenShareAutoRotate is on - the viewer’s whole screen rotates to the sharer’s orientation.

The follow-through also handles 180° flips within one orientation (phone upside-down).

On the sharer’s own screen

While you share, CallScreen shows a ScreenShareCover instead of the call - a card explaining the screen is being shared, with a warning if your camera is also off (the peer would see nothing but your screen). This keeps your call UI out of the shared frame.

PiP is suppressed while sharing

MainActivity.enterPipMode() bails out when isScreenSharingState is set:

fun enterPipMode(): Boolean { if (SDK_INT >= O && inCallState && !isScreenSharingState) { … } return false }

The PiP window would float on top of whatever you’re presenting and get captured back into the share (the peer sees their own video echoed in your screen). The foreground service keeps the call alive in the background regardless, and the CallStyle notification carries the controls. When you stop sharing, PiP works normally again.

Stopping

stopScreenShare() stops the audio capturer, stops+disposes the screen capturer, disposes the dedicated helper, and calls ensureCapturing() to bring the camera back. It runs on an explicit tap, on the system’s MediaProjection.Callback.onStop, and on call teardown.