Skip to main content
Version: 5.5

Screen share in a group call

This page provides a code example for implementing screen share in a group call (conference).

This document demonstrates how to implement screen share functionality in a service using the MediaProjection API. The implementation supports maintaining screen share even when the app is in the background.

Prerequisites

Before you begin, you must do the following.

Add required permissions

In your AndroidManifest.xml, include the following permissions:

<!-- For Foreground Service -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

<!-- For Screen Capture on Android 14+ -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />

Register the foreground service

Declare the service by specifying mediaProjection foreground service type so that the app can maintain screen share even when it is in the background.

<service
android:name=".MyScreenCaptureService"
android:foregroundServiceType="mediaProjection" />
Tip

For more information on foreground services, refer to Android documentation (Foreground services, Foreground service types are required).

Sender - Implement the screen capture service

This service handles the start and stop operations for screen capture using MediaProjection, integrating with the group call.

class MyScreenCaptureService : Service() {
companion object {
private const val SCREEN_CAPTURE_ID = "Screen capture"

// This can be any dummy number
private const val NOTIFICATION_ID = 315

private const val ACTION_START_SCREEN_CAPTURE = "ACTION_START_SCREEN_CAPTURE"
private const val ACTION_STOP_SCREEN_CAPTURE = "ACTION_STOP_SCREEN_CAPTURE"
private lateinit var mediaProjectionResult:ActivityResult
private var subgroupName: String? = null

@Synchronized
@JvmStatic
fun startScreenCapturing(context: Context, projectionPermissionResult: ActivityResult, destSubgroupName:String? = null) {
val intent = Intent(context, MyScreenCaptureService::class.java).apply {
action = ACTION_START_SCREEN_CAPTURE
mediaProjectionResult = projectionPermissionResult
subgroupName = destSubgroupName
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}

@Synchronized
@JvmStatic
fun stopScreenCapturing(context: Context) {
val intent = Intent(context, MyScreenCaptureService::class.java).apply {
action = ACTION_STOP_SCREEN_CAPTURE
}
context.startService(intent)
}
}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val action = intent?.action ?: return START_NOT_STICKY
when (action) {
ACTION_START_SCREEN_CAPTURE -> {
onScreenShareStarted()
}
ACTION_STOP_SCREEN_CAPTURE -> {
onScreenShareStopped()
}
}
return START_NOT_STICKY
}

private fun onScreenShareStarted() {
val notification = createNotification()

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION)
} else {
startForeground(NOTIFICATION_ID, notification)
}

mediaProjectionResult.data?.let { data ->
val screenSource = ScreenCapturerVideoSource.getInstance(mediaProjectionResult.resultCode, data)
screenSource.setOnErrorListener { onScreenShareStopped() }
PlanetKit.getConference()?.let {
it.startMyScreenShare(screenSource, subgroupName) { response ->
if (!response.isSuccessful) {
onScreenShareStopped()
}
}
}
}
}

private fun onScreenShareStopped() {
PlanetKit.getConference()?.let {
it.stopMyScreenShare()
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
stopForeground(STOP_FOREGROUND_REMOVE)
}
stopSelf()
}

private fun createNotification(): Notification {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
SCREEN_CAPTURE_ID,
SCREEN_CAPTURE_ID,
NotificationManager.IMPORTANCE_NONE
)
// Register the channel with the system
val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}

return NotificationCompat.Builder(
this,
SCREEN_CAPTURE_ID
)
.setSmallIcon(R.drawable.ic_stat_name)
.setContentTitle("Screen share in progress")
.setPriority(NotificationCompat.PRIORITY_LOW)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.build()
}
}

Sender - Manage screen capture in an activity

This activity demonstrates how to request screen capture permissions and start or stop the service.

class MyConferenceActivity : AppCompatActivity() {
private var myScreenShareSubgroupName: String? = null
private val requestMediaProjectionActivityResultLauncher: ActivityResultLauncher<Intent> =
registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { projectionPermissionResult: ActivityResult ->
val data = projectionPermissionResult.data
if (projectionPermissionResult.resultCode != RESULT_OK || data == null) {
Log.e(TAG, "Could not start screen capture!")
return@registerForActivityResult
}
MyScreenCaptureService.startScreenCapturing(context, permissionResult, myScreenShareSubgroupName)
}


private fun requestMyScreenCapture(subgroupName: String?) {
myScreenShareSubgroupName = subgroupName
val mediaProjectionManager =
getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager

// This initiates a prompt dialog for the user to confirm screen projection.
requestMediaProjectionActivityResultLauncher.launch(mediaProjectionManager.createScreenCaptureIntent())
}

fun stopMyScreenCapture(context: Context) {
MyScreenCaptureService.stopScreenCapturing(context)
}
}

Sender - Change the destination of screen share

Depending on the type of destination, you can change the destination of screen share as follows:

  • To change the destination to a subgroup other than the main room, call changeMyScreenShareDestination() with the subgroup name.
  • To change the destination to the main room, call changeMyScreenShareDestinationToMainRoom().
// Change the destination to a subgroup other than the main room
conference?.changeMyScreenShareDestination(subgroupName) {
Log.d("Conference", "changeMyScreenShareDestination onResponse: $it")
if(it.isSuccessful) {
...
}
else {
...
}
}

// Change the destination to the main room
conference?.changeMyScreenShareDestinationToMainRoom() {
Log.d("Conference", "changeMyScreenShareDestinationToMainRoom onResponse: $it")
if(it.isSuccessful) {
...
}
else {
...
}
}

Receiver - Receive screen share update events

First of all, you need to check whether the peer's screen share has started or stopped through the onScreenShareUpdated event of PeerControlListener.

class PeerContainer internal constructor(
peer: PlanetKitConferencePeer
) {
val peerControl: PlanetKitPeerControl = peer.createPeerControl()
?: throw IllegalStateException("Failed to create peer control.")

init {
val peerControlListener = object : PlanetKitPeerControl.PeerControlListener {
...

override fun onScreenShareUpdated(peer: PlanetKitConferencePeer, state: PlanetKitScreenShareState, subgroupName: String?) {
Log.d("Conference", "onScreenShareUpdated: user=(${peer.user}), screenShareState=$screenShareState, subgroup=$subgroupName")
}
}

peerControl.register(peerControlListener)
}
}

Receiver - Start a peer's screen share video

To start a peer's screen share video, call startScreenShare() of PlanetKitPeerControl.

var peer: PlanetKitUser
var videoView: PlanetKitVideoView

conference.addPeerScreenShareView(peer, videoView)
videoView.resetFirstFrameRendered()
peerControl?.startScreenShare(subgroupName, { response ->
Log.d("Conference", "isSuccessful=${response.isSuccessful}")
})

Receiver - Stop a peer's screen share video

To stop a peer's screen share video, call stopScreenShare() of PlanetKitPeerControl.

peerControl?.stopScreenShare(null, { response ->
Log.d("Conference", "isSuccessful=${response.isSuccessful}")
})

conference?.removePeerScreenShareView(peer, videoView)