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
  1 +---
  2 +"client-sdk-android": patch
  3 +---
  4 +
  5 +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 @@ -30,6 +30,7 @@ import io.livekit.android.audio.ScreenAudioCapturer
30 import io.livekit.android.room.track.LocalAudioTrack 30 import io.livekit.android.room.track.LocalAudioTrack
31 import io.livekit.android.room.track.LocalVideoTrack 31 import io.livekit.android.room.track.LocalVideoTrack
32 import io.livekit.android.room.track.Track 32 import io.livekit.android.room.track.Track
  33 +import io.livekit.android.room.track.screencapture.ScreenCaptureParams
33 import io.livekit.android.sample.service.ForegroundService 34 import io.livekit.android.sample.service.ForegroundService
34 import kotlinx.coroutines.Dispatchers 35 import kotlinx.coroutines.Dispatchers
35 import kotlinx.coroutines.launch 36 import kotlinx.coroutines.launch
@@ -59,7 +60,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { @@ -59,7 +60,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
59 return@launch 60 return@launch
60 } 61 }
61 62
62 - room.localParticipant.setScreenShareEnabled(true, data) 63 + room.localParticipant.setScreenShareEnabled(true, ScreenCaptureParams(data))
63 64
64 // Optionally disable the mic for screenshare audio only 65 // Optionally disable the mic for screenshare audio only
65 // val javaAudioDeviceModule = (room.lkObjects.audioDeviceModule as? JavaAudioDeviceModule) 66 // val javaAudioDeviceModule = (room.lkObjects.audioDeviceModule as? JavaAudioDeviceModule)
@@ -50,6 +50,7 @@ import io.livekit.android.room.track.TrackPublication @@ -50,6 +50,7 @@ import io.livekit.android.room.track.TrackPublication
50 import io.livekit.android.room.track.VideoCaptureParameter 50 import io.livekit.android.room.track.VideoCaptureParameter
51 import io.livekit.android.room.track.VideoCodec 51 import io.livekit.android.room.track.VideoCodec
52 import io.livekit.android.room.track.VideoEncoding 52 import io.livekit.android.room.track.VideoEncoding
  53 +import io.livekit.android.room.track.screencapture.ScreenCaptureParams
53 import io.livekit.android.room.util.EncodingUtils 54 import io.livekit.android.room.util.EncodingUtils
54 import io.livekit.android.rpc.RpcError 55 import io.livekit.android.rpc.RpcError
55 import io.livekit.android.util.LKLog 56 import io.livekit.android.util.LKLog
@@ -222,6 +223,7 @@ internal constructor( @@ -222,6 +223,7 @@ internal constructor(
222 mediaProjectionPermissionResultData: Intent, 223 mediaProjectionPermissionResultData: Intent,
223 options: LocalVideoTrackOptions = screenShareTrackCaptureDefaults.copy(), 224 options: LocalVideoTrackOptions = screenShareTrackCaptureDefaults.copy(),
224 videoProcessor: VideoProcessor? = null, 225 videoProcessor: VideoProcessor? = null,
  226 + onStop: (Track) -> Unit,
225 ): LocalScreencastVideoTrack { 227 ): LocalScreencastVideoTrack {
226 val screencastOptions = options.copy(isScreencast = true) 228 val screencastOptions = options.copy(isScreencast = true)
227 return LocalScreencastVideoTrack.createTrack( 229 return LocalScreencastVideoTrack.createTrack(
@@ -233,6 +235,7 @@ internal constructor( @@ -233,6 +235,7 @@ internal constructor(
233 eglBase, 235 eglBase,
234 screencastVideoTrackFactory, 236 screencastVideoTrackFactory,
235 videoProcessor, 237 videoProcessor,
  238 + onStop,
236 ) 239 )
237 } 240 }
238 241
@@ -293,15 +296,15 @@ internal constructor( @@ -293,15 +296,15 @@ internal constructor(
293 @Throws(TrackException.PublishException::class) 296 @Throws(TrackException.PublishException::class)
294 suspend fun setScreenShareEnabled( 297 suspend fun setScreenShareEnabled(
295 enabled: Boolean, 298 enabled: Boolean,
296 - mediaProjectionPermissionResultData: Intent? = null, 299 + screenCaptureParams: ScreenCaptureParams? = null,
297 ) { 300 ) {
298 - setTrackEnabled(Track.Source.SCREEN_SHARE, enabled, mediaProjectionPermissionResultData) 301 + setTrackEnabled(Track.Source.SCREEN_SHARE, enabled, screenCaptureParams)
299 } 302 }
300 303
301 private suspend fun setTrackEnabled( 304 private suspend fun setTrackEnabled(
302 source: Track.Source, 305 source: Track.Source,
303 enabled: Boolean, 306 enabled: Boolean,
304 - mediaProjectionPermissionResultData: Intent? = null, 307 + screenCaptureParams: ScreenCaptureParams? = null,
305 ) { 308 ) {
306 val pubLock = sourcePubLocks[source]!! 309 val pubLock = sourcePubLocks[source]!!
307 pubLock.withLock { 310 pubLock.withLock {
@@ -327,12 +330,15 @@ internal constructor( @@ -327,12 +330,15 @@ internal constructor(
327 } 330 }
328 331
329 Track.Source.SCREEN_SHARE -> { 332 Track.Source.SCREEN_SHARE -> {
330 - if (mediaProjectionPermissionResultData == null) {  
331 - throw IllegalArgumentException("Media Projection permission result data is required to create a screen share track.") 333 + if (screenCaptureParams == null) {
  334 + throw IllegalArgumentException("Media Projection params is required to create a screen share track.")
332 } 335 }
333 val track = 336 val track =
334 - createScreencastTrack(mediaProjectionPermissionResultData = mediaProjectionPermissionResultData)  
335 - track.startForegroundService(null, null) 337 + createScreencastTrack(mediaProjectionPermissionResultData = screenCaptureParams.mediaProjectionPermissionResultData) {
  338 + unpublishTrack(it)
  339 + screenCaptureParams.onStop?.invoke()
  340 + }
  341 + track.startForegroundService(screenCaptureParams.notificationId, screenCaptureParams.notification)
336 track.startCapture() 342 track.startCapture()
337 publishVideoTrack(track, options = VideoTrackPublishOptions(null, screenShareTrackPublishDefaults)) 343 publishVideoTrack(track, options = VideoTrackPublishOptions(null, screenShareTrackPublishDefaults))
338 } 344 }
1 /* 1 /*
2 - * Copyright 2023-2024 LiveKit, Inc. 2 + * Copyright 2023-2025 LiveKit, Inc.
3 * 3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License. 5 * you may not use this file except in compliance with the License.
@@ -151,7 +151,10 @@ constructor( @@ -151,7 +151,10 @@ constructor(
151 private val serviceConnection = ScreenCaptureConnection(context) 151 private val serviceConnection = ScreenCaptureConnection(context)
152 152
153 init { 153 init {
154 - mediaProjectionCallback.onStopCallback = { stop() } 154 + mediaProjectionCallback.apply {
  155 + track = this@LocalScreencastVideoTrack
  156 + addOnStopCallback { stop() }
  157 + }
155 } 158 }
156 159
157 /** 160 /**
@@ -203,11 +206,18 @@ constructor( @@ -203,11 +206,18 @@ constructor(
203 /** 206 /**
204 * Needed to deal with circular dependency. 207 * Needed to deal with circular dependency.
205 */ 208 */
206 - class MediaProjectionCallback : MediaProjection.Callback() {  
207 - var onStopCallback: (() -> Unit)? = null 209 + class MediaProjectionCallback() : MediaProjection.Callback() {
  210 +
  211 + var track: Track? = null
  212 +
  213 + private val onStopCallbacks = mutableListOf<(Track) -> Unit>()
  214 +
  215 + fun addOnStopCallback(callback: (Track) -> Unit) {
  216 + onStopCallbacks.add(callback)
  217 + }
208 218
209 override fun onStop() { 219 override fun onStop() {
210 - onStopCallback?.invoke() 220 + onStopCallbacks.forEach { it.invoke(track!!) }
211 } 221 }
212 } 222 }
213 223
@@ -221,10 +231,13 @@ constructor( @@ -221,10 +231,13 @@ constructor(
221 rootEglBase: EglBase, 231 rootEglBase: EglBase,
222 screencastVideoTrackFactory: Factory, 232 screencastVideoTrackFactory: Factory,
223 videoProcessor: VideoProcessor?, 233 videoProcessor: VideoProcessor?,
  234 + onStop: (Track) -> Unit,
224 ): LocalScreencastVideoTrack { 235 ): LocalScreencastVideoTrack {
225 val source = peerConnectionFactory.createVideoSource(options.isScreencast) 236 val source = peerConnectionFactory.createVideoSource(options.isScreencast)
226 source.setVideoProcessor(videoProcessor) 237 source.setVideoProcessor(videoProcessor)
227 - val callback = MediaProjectionCallback() 238 + val callback = MediaProjectionCallback().apply {
  239 + addOnStopCallback(onStop)
  240 + }
228 val capturer = createScreenCapturer(mediaProjectionPermissionResultData, callback) 241 val capturer = createScreenCapturer(mediaProjectionPermissionResultData, callback)
229 capturer.initialize( 242 capturer.initialize(
230 SurfaceTextureHelper.create("ScreenVideoCaptureThread", rootEglBase.eglBaseContext), 243 SurfaceTextureHelper.create("ScreenVideoCaptureThread", rootEglBase.eglBaseContext),
  1 +/*
  2 + * Copyright 2025 LiveKit, Inc.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package io.livekit.android.room.track.screencapture
  18 +
  19 +import android.app.Notification
  20 +import android.content.Intent
  21 +
  22 +class ScreenCaptureParams(
  23 + val mediaProjectionPermissionResultData: Intent,
  24 + val notificationId: Int? = null,
  25 + val notification: Notification? = null,
  26 + val onStop: (() -> Unit)? = null
  27 +)
1 /* 1 /*
2 - * Copyright 2023-2024 LiveKit, Inc. 2 + * Copyright 2023-2025 LiveKit, Inc.
3 * 3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License. 5 * you may not use this file except in compliance with the License.
@@ -46,6 +46,7 @@ import io.livekit.android.room.track.CameraPosition @@ -46,6 +46,7 @@ import io.livekit.android.room.track.CameraPosition
46 import io.livekit.android.room.track.LocalScreencastVideoTrack 46 import io.livekit.android.room.track.LocalScreencastVideoTrack
47 import io.livekit.android.room.track.LocalVideoTrack 47 import io.livekit.android.room.track.LocalVideoTrack
48 import io.livekit.android.room.track.Track 48 import io.livekit.android.room.track.Track
  49 +import io.livekit.android.room.track.screencapture.ScreenCaptureParams
49 import io.livekit.android.room.track.video.CameraCapturerUtils 50 import io.livekit.android.room.track.video.CameraCapturerUtils
50 import io.livekit.android.sample.model.StressTest 51 import io.livekit.android.sample.model.StressTest
51 import io.livekit.android.sample.service.ForegroundService 52 import io.livekit.android.sample.service.ForegroundService
@@ -308,7 +309,7 @@ class CallViewModel( @@ -308,7 +309,7 @@ class CallViewModel(
308 fun startScreenCapture(mediaProjectionPermissionResultData: Intent) { 309 fun startScreenCapture(mediaProjectionPermissionResultData: Intent) {
309 val localParticipant = room.localParticipant 310 val localParticipant = room.localParticipant
310 viewModelScope.launch { 311 viewModelScope.launch {
311 - localParticipant.setScreenShareEnabled(true, mediaProjectionPermissionResultData) 312 + localParticipant.setScreenShareEnabled(true, ScreenCaptureParams(mediaProjectionPermissionResultData))
312 val screencastTrack = localParticipant.getTrackPublication(Track.Source.SCREEN_SHARE)?.track as? LocalScreencastVideoTrack 313 val screencastTrack = localParticipant.getTrackPublication(Track.Source.SCREEN_SHARE)?.track as? LocalScreencastVideoTrack
313 this@CallViewModel.localScreencastTrack = screencastTrack 314 this@CallViewModel.localScreencastTrack = screencastTrack
314 mutableScreencastEnabled.postValue(screencastTrack?.enabled) 315 mutableScreencastEnabled.postValue(screencastTrack?.enabled)