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" />
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)