Jean Kruger
Committed by GitHub

Unpublish the screen sharing track on stop and introduce ScreenCaptureParams (#626)

* Unpublish the screen sharing track on stop

* Introduce ScreenCaptureParams

* Add changeset

* Lint fixes
---
"client-sdk-android": patch
---
Unpublish the screen sharing track on stop and introduce ScreenCaptureParams to be able to define the notification and set a callback for onStop
... ...
... ... @@ -30,6 +30,7 @@ import io.livekit.android.audio.ScreenAudioCapturer
import io.livekit.android.room.track.LocalAudioTrack
import io.livekit.android.room.track.LocalVideoTrack
import io.livekit.android.room.track.Track
import io.livekit.android.room.track.screencapture.ScreenCaptureParams
import io.livekit.android.sample.service.ForegroundService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
... ... @@ -59,7 +60,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
return@launch
}
room.localParticipant.setScreenShareEnabled(true, data)
room.localParticipant.setScreenShareEnabled(true, ScreenCaptureParams(data))
// Optionally disable the mic for screenshare audio only
// val javaAudioDeviceModule = (room.lkObjects.audioDeviceModule as? JavaAudioDeviceModule)
... ...
... ... @@ -50,6 +50,7 @@ import io.livekit.android.room.track.TrackPublication
import io.livekit.android.room.track.VideoCaptureParameter
import io.livekit.android.room.track.VideoCodec
import io.livekit.android.room.track.VideoEncoding
import io.livekit.android.room.track.screencapture.ScreenCaptureParams
import io.livekit.android.room.util.EncodingUtils
import io.livekit.android.rpc.RpcError
import io.livekit.android.util.LKLog
... ... @@ -222,6 +223,7 @@ internal constructor(
mediaProjectionPermissionResultData: Intent,
options: LocalVideoTrackOptions = screenShareTrackCaptureDefaults.copy(),
videoProcessor: VideoProcessor? = null,
onStop: (Track) -> Unit,
): LocalScreencastVideoTrack {
val screencastOptions = options.copy(isScreencast = true)
return LocalScreencastVideoTrack.createTrack(
... ... @@ -233,6 +235,7 @@ internal constructor(
eglBase,
screencastVideoTrackFactory,
videoProcessor,
onStop,
)
}
... ... @@ -293,15 +296,15 @@ internal constructor(
@Throws(TrackException.PublishException::class)
suspend fun setScreenShareEnabled(
enabled: Boolean,
mediaProjectionPermissionResultData: Intent? = null,
screenCaptureParams: ScreenCaptureParams? = null,
) {
setTrackEnabled(Track.Source.SCREEN_SHARE, enabled, mediaProjectionPermissionResultData)
setTrackEnabled(Track.Source.SCREEN_SHARE, enabled, screenCaptureParams)
}
private suspend fun setTrackEnabled(
source: Track.Source,
enabled: Boolean,
mediaProjectionPermissionResultData: Intent? = null,
screenCaptureParams: ScreenCaptureParams? = null,
) {
val pubLock = sourcePubLocks[source]!!
pubLock.withLock {
... ... @@ -327,12 +330,15 @@ internal constructor(
}
Track.Source.SCREEN_SHARE -> {
if (mediaProjectionPermissionResultData == null) {
throw IllegalArgumentException("Media Projection permission result data is required to create a screen share track.")
if (screenCaptureParams == null) {
throw IllegalArgumentException("Media Projection params is required to create a screen share track.")
}
val track =
createScreencastTrack(mediaProjectionPermissionResultData = mediaProjectionPermissionResultData)
track.startForegroundService(null, null)
createScreencastTrack(mediaProjectionPermissionResultData = screenCaptureParams.mediaProjectionPermissionResultData) {
unpublishTrack(it)
screenCaptureParams.onStop?.invoke()
}
track.startForegroundService(screenCaptureParams.notificationId, screenCaptureParams.notification)
track.startCapture()
publishVideoTrack(track, options = VideoTrackPublishOptions(null, screenShareTrackPublishDefaults))
}
... ...
/*
* Copyright 2023-2024 LiveKit, Inc.
* Copyright 2023-2025 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
... ... @@ -151,7 +151,10 @@ constructor(
private val serviceConnection = ScreenCaptureConnection(context)
init {
mediaProjectionCallback.onStopCallback = { stop() }
mediaProjectionCallback.apply {
track = this@LocalScreencastVideoTrack
addOnStopCallback { stop() }
}
}
/**
... ... @@ -203,11 +206,18 @@ constructor(
/**
* Needed to deal with circular dependency.
*/
class MediaProjectionCallback : MediaProjection.Callback() {
var onStopCallback: (() -> Unit)? = null
class MediaProjectionCallback() : MediaProjection.Callback() {
var track: Track? = null
private val onStopCallbacks = mutableListOf<(Track) -> Unit>()
fun addOnStopCallback(callback: (Track) -> Unit) {
onStopCallbacks.add(callback)
}
override fun onStop() {
onStopCallback?.invoke()
onStopCallbacks.forEach { it.invoke(track!!) }
}
}
... ... @@ -221,10 +231,13 @@ constructor(
rootEglBase: EglBase,
screencastVideoTrackFactory: Factory,
videoProcessor: VideoProcessor?,
onStop: (Track) -> Unit,
): LocalScreencastVideoTrack {
val source = peerConnectionFactory.createVideoSource(options.isScreencast)
source.setVideoProcessor(videoProcessor)
val callback = MediaProjectionCallback()
val callback = MediaProjectionCallback().apply {
addOnStopCallback(onStop)
}
val capturer = createScreenCapturer(mediaProjectionPermissionResultData, callback)
capturer.initialize(
SurfaceTextureHelper.create("ScreenVideoCaptureThread", rootEglBase.eglBaseContext),
... ...
/*
* Copyright 2025 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.livekit.android.room.track.screencapture
import android.app.Notification
import android.content.Intent
class ScreenCaptureParams(
val mediaProjectionPermissionResultData: Intent,
val notificationId: Int? = null,
val notification: Notification? = null,
val onStop: (() -> Unit)? = null
)
... ...
/*
* Copyright 2023-2024 LiveKit, Inc.
* Copyright 2023-2025 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
... ... @@ -46,6 +46,7 @@ import io.livekit.android.room.track.CameraPosition
import io.livekit.android.room.track.LocalScreencastVideoTrack
import io.livekit.android.room.track.LocalVideoTrack
import io.livekit.android.room.track.Track
import io.livekit.android.room.track.screencapture.ScreenCaptureParams
import io.livekit.android.room.track.video.CameraCapturerUtils
import io.livekit.android.sample.model.StressTest
import io.livekit.android.sample.service.ForegroundService
... ... @@ -308,7 +309,7 @@ class CallViewModel(
fun startScreenCapture(mediaProjectionPermissionResultData: Intent) {
val localParticipant = room.localParticipant
viewModelScope.launch {
localParticipant.setScreenShareEnabled(true, mediaProjectionPermissionResultData)
localParticipant.setScreenShareEnabled(true, ScreenCaptureParams(mediaProjectionPermissionResultData))
val screencastTrack = localParticipant.getTrackPublication(Track.Source.SCREEN_SHARE)?.track as? LocalScreencastVideoTrack
this@CallViewModel.localScreencastTrack = screencastTrack
mutableScreencastEnabled.postValue(screencastTrack?.enabled)
... ...