davidliu
Committed by GitHub

android screenshare as video track (#16)

* screencast implementation

* update usage comment

* fill in track source parameter

* Make screenCaptureConnection.stop() idempotent

* minor cleanup

* update compose sample application to include screenshare

* fix tests
正在显示 19 个修改的文件 包含 431 行增加22 行删除
@@ -2,8 +2,8 @@ @@ -2,8 +2,8 @@
2 2
3 buildscript { 3 buildscript {
4 ext { 4 ext {
5 - compose_version = '1.0.3'  
6 - kotlin_version = '1.5.30' 5 + compose_version = '1.0.4'
  6 + kotlin_version = '1.5.31'
7 java_version = JavaVersion.VERSION_1_8 7 java_version = JavaVersion.VERSION_1_8
8 dokka_version = '1.5.0' 8 dokka_version = '1.5.0'
9 } 9 }
@@ -5,5 +5,13 @@ @@ -5,5 +5,13 @@
5 <uses-permission android:name="android.permission.INTERNET" /> 5 <uses-permission android:name="android.permission.INTERNET" />
6 <uses-permission android:name="android.permission.RECORD_AUDIO" /> 6 <uses-permission android:name="android.permission.RECORD_AUDIO" />
7 <uses-permission android:name="android.permission.CAMERA" /> 7 <uses-permission android:name="android.permission.CAMERA" />
  8 + <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
8 9
  10 + <application>
  11 + <service
  12 + android:name="io.livekit.android.room.track.screencapture.ScreenCaptureService"
  13 + android:enabled="true"
  14 + android:foregroundServiceType="mediaProjection"
  15 + android:stopWithTask="true" />
  16 + </application>
9 </manifest> 17 </manifest>
@@ -109,7 +109,7 @@ internal constructor( @@ -109,7 +109,7 @@ internal constructor(
109 return joinResponse 109 return joinResponse
110 } 110 }
111 111
112 - private suspend fun configure(joinResponse: LivekitRtc.JoinResponse) { 112 + private fun configure(joinResponse: LivekitRtc.JoinResponse) {
113 if (this::publisher.isInitialized || this::subscriber.isInitialized) { 113 if (this::publisher.isInitialized || this::subscriber.isInitialized) {
114 // already configured 114 // already configured
115 return 115 return
@@ -2,6 +2,7 @@ package io.livekit.android.room.participant @@ -2,6 +2,7 @@ package io.livekit.android.room.participant
2 2
3 import android.Manifest 3 import android.Manifest
4 import android.content.Context 4 import android.content.Context
  5 +import android.content.Intent
5 import com.google.protobuf.ByteString 6 import com.google.protobuf.ByteString
6 import dagger.assisted.Assisted 7 import dagger.assisted.Assisted
7 import dagger.assisted.AssistedFactory 8 import dagger.assisted.AssistedFactory
@@ -23,6 +24,7 @@ internal constructor( @@ -23,6 +24,7 @@ internal constructor(
23 private val peerConnectionFactory: PeerConnectionFactory, 24 private val peerConnectionFactory: PeerConnectionFactory,
24 private val context: Context, 25 private val context: Context,
25 private val eglBase: EglBase, 26 private val eglBase: EglBase,
  27 + private val screencastVideoTrackFactory: LocalScreencastVideoTrack.Factory,
26 ) : 28 ) :
27 Participant(info.sid, info.identity) { 29 Participant(info.sid, info.identity) {
28 30
@@ -72,6 +74,27 @@ internal constructor( @@ -72,6 +74,27 @@ internal constructor(
72 ) 74 )
73 } 75 }
74 76
  77 + /**
  78 + * Creates a screencast video track.
  79 + *
  80 + * @param mediaProjectionPermissionResultData The resultData returned from launching
  81 + * [MediaProjectionManager.createScreenCaptureIntent()](https://developer.android.com/reference/android/media/projection/MediaProjectionManager#createScreenCaptureIntent()).
  82 + */
  83 + fun createScreencastTrack(
  84 + name: String = "",
  85 + mediaProjectionPermissionResultData: Intent,
  86 + ): LocalScreencastVideoTrack {
  87 + return LocalScreencastVideoTrack.createTrack(
  88 + mediaProjectionPermissionResultData,
  89 + peerConnectionFactory,
  90 + context,
  91 + name,
  92 + LocalVideoTrackOptions(isScreencast = true),
  93 + eglBase,
  94 + screencastVideoTrackFactory
  95 + )
  96 + }
  97 +
75 suspend fun publishAudioTrack( 98 suspend fun publishAudioTrack(
76 track: LocalAudioTrack, 99 track: LocalAudioTrack,
77 options: AudioTrackPublishOptions = AudioTrackPublishOptions(), 100 options: AudioTrackPublishOptions = AudioTrackPublishOptions(),
@@ -85,6 +108,7 @@ internal constructor( @@ -85,6 +108,7 @@ internal constructor(
85 val cid = track.rtcTrack.id() 108 val cid = track.rtcTrack.id()
86 val builder = LivekitRtc.AddTrackRequest.newBuilder().apply { 109 val builder = LivekitRtc.AddTrackRequest.newBuilder().apply {
87 disableDtx = !options.dtx 110 disableDtx = !options.dtx
  111 + source = LivekitModels.TrackSource.MICROPHONE
88 } 112 }
89 val trackInfo = engine.addTrack( 113 val trackInfo = engine.addTrack(
90 cid = cid, 114 cid = cid,
@@ -124,6 +148,11 @@ internal constructor( @@ -124,6 +148,11 @@ internal constructor(
124 val builder = LivekitRtc.AddTrackRequest.newBuilder().apply { 148 val builder = LivekitRtc.AddTrackRequest.newBuilder().apply {
125 width = track.dimensions.width 149 width = track.dimensions.width
126 height = track.dimensions.height 150 height = track.dimensions.height
  151 + source = if(track.options.isScreencast){
  152 + LivekitModels.TrackSource.SCREEN_SHARE
  153 + } else {
  154 + LivekitModels.TrackSource.CAMERA
  155 + }
127 } 156 }
128 val trackInfo = engine.addTrack( 157 val trackInfo = engine.addTrack(
129 cid = cid, 158 cid = cid,
  1 +package io.livekit.android.room.track
  2 +
  3 +import android.app.Notification
  4 +import android.content.Context
  5 +import android.content.Intent
  6 +import android.media.projection.MediaProjection
  7 +import dagger.assisted.Assisted
  8 +import dagger.assisted.AssistedFactory
  9 +import dagger.assisted.AssistedInject
  10 +import io.livekit.android.room.track.screencapture.ScreenCaptureConnection
  11 +import org.webrtc.*
  12 +import java.util.*
  13 +
  14 +class LocalScreencastVideoTrack
  15 +@AssistedInject
  16 +constructor(
  17 + @Assisted capturer: VideoCapturer,
  18 + @Assisted source: VideoSource,
  19 + @Assisted name: String,
  20 + @Assisted options: LocalVideoTrackOptions,
  21 + @Assisted rtcTrack: org.webrtc.VideoTrack,
  22 + @Assisted mediaProjectionCallback: MediaProjectionCallback,
  23 + peerConnectionFactory: PeerConnectionFactory,
  24 + context: Context,
  25 + eglBase: EglBase,
  26 +) : LocalVideoTrack(
  27 + capturer,
  28 + source,
  29 + name,
  30 + options,
  31 + rtcTrack,
  32 + peerConnectionFactory,
  33 + context,
  34 + eglBase
  35 +) {
  36 +
  37 + private val serviceConnection = ScreenCaptureConnection(context)
  38 +
  39 + init {
  40 + mediaProjectionCallback.onStopCallback = { stop() }
  41 + }
  42 +
  43 + /**
  44 + * A foreground service is generally required prior to [startCapture]. This method starts up
  45 + * a helper foreground service that only serves to display a notification while capturing. This
  46 + * foreground service will stop upon the end of screen capture.
  47 + *
  48 + * You may choose to use your own foreground service instead of this method, but it must be
  49 + * started prior to calling [startCapture].
  50 + *
  51 + * @see [io.livekit.android.room.track.screencapture.ScreenCaptureService.start]
  52 + */
  53 + suspend fun startForegroundService(notificationId: Int?, notification: Notification?) {
  54 + serviceConnection.connect()
  55 + serviceConnection.startForeground(notificationId, notification)
  56 + }
  57 +
  58 + override fun stop() {
  59 + super.stop()
  60 + serviceConnection.stop()
  61 + }
  62 +
  63 + @AssistedFactory
  64 + interface Factory {
  65 + fun create(
  66 + capturer: VideoCapturer,
  67 + source: VideoSource,
  68 + name: String,
  69 + options: LocalVideoTrackOptions,
  70 + rtcTrack: org.webrtc.VideoTrack,
  71 + mediaProjectionCallback: MediaProjectionCallback,
  72 + ): LocalScreencastVideoTrack
  73 + }
  74 +
  75 + /**
  76 + * Needed to deal with circular dependency.
  77 + */
  78 + class MediaProjectionCallback : MediaProjection.Callback() {
  79 + var onStopCallback: (() -> Unit)? = null
  80 +
  81 + override fun onStop() {
  82 + onStopCallback?.invoke()
  83 + }
  84 + }
  85 +
  86 + companion object {
  87 + internal fun createTrack(
  88 + mediaProjectionPermissionResultData: Intent,
  89 + peerConnectionFactory: PeerConnectionFactory,
  90 + context: Context,
  91 + name: String,
  92 + options: LocalVideoTrackOptions,
  93 + rootEglBase: EglBase,
  94 + screencastVideoTrackFactory: Factory
  95 + ): LocalScreencastVideoTrack {
  96 + val source = peerConnectionFactory.createVideoSource(options.isScreencast)
  97 + val callback = MediaProjectionCallback()
  98 + val capturer = createScreenCapturer(mediaProjectionPermissionResultData, callback)
  99 + capturer.initialize(
  100 + SurfaceTextureHelper.create("ScreenVideoCaptureThread", rootEglBase.eglBaseContext),
  101 + context,
  102 + source.capturerObserver
  103 + )
  104 + val track = peerConnectionFactory.createVideoTrack(UUID.randomUUID().toString(), source)
  105 +
  106 + return screencastVideoTrackFactory.create(
  107 + capturer = capturer,
  108 + source = source,
  109 + options = options,
  110 + name = name,
  111 + rtcTrack = track,
  112 + mediaProjectionCallback = callback
  113 + )
  114 + }
  115 +
  116 +
  117 + private fun createScreenCapturer(
  118 + resultData: Intent,
  119 + callback: MediaProjectionCallback
  120 + ): ScreenCapturerAndroid {
  121 + return ScreenCapturerAndroid(resultData, callback)
  122 + }
  123 + }
  124 +}
@@ -8,12 +8,13 @@ import io.livekit.android.util.LKLog @@ -8,12 +8,13 @@ import io.livekit.android.util.LKLog
8 import org.webrtc.* 8 import org.webrtc.*
9 import java.util.* 9 import java.util.*
10 10
  11 +
11 /** 12 /**
12 * A representation of a local video track (generally input coming from camera or screen). 13 * A representation of a local video track (generally input coming from camera or screen).
13 * 14 *
14 * [startCapture] should be called before use. 15 * [startCapture] should be called before use.
15 */ 16 */
16 -class LocalVideoTrack( 17 +open class LocalVideoTrack(
17 private var capturer: VideoCapturer, 18 private var capturer: VideoCapturer,
18 private var source: VideoSource, 19 private var source: VideoSource,
19 name: String, 20 name: String,
@@ -40,7 +41,7 @@ class LocalVideoTrack( @@ -40,7 +41,7 @@ class LocalVideoTrack(
40 private val sender: RtpSender? 41 private val sender: RtpSender?
41 get() = transceiver?.sender 42 get() = transceiver?.sender
42 43
43 - fun startCapture() { 44 + open fun startCapture() {
44 capturer.startCapture( 45 capturer.startCapture(
45 options.captureParams.width, 46 options.captureParams.width,
46 options.captureParams.height, 47 options.captureParams.height,
@@ -157,5 +158,6 @@ class LocalVideoTrack( @@ -157,5 +158,6 @@ class LocalVideoTrack(
157 } 158 }
158 return null 159 return null
159 } 160 }
  161 +
160 } 162 }
161 } 163 }
  1 +package io.livekit.android.room.track.screencapture
  2 +
  3 +import android.app.Notification
  4 +import android.content.ComponentName
  5 +import android.content.Context
  6 +import android.content.Context.BIND_AUTO_CREATE
  7 +import android.content.Intent
  8 +import android.content.ServiceConnection
  9 +import android.os.IBinder
  10 +import io.livekit.android.util.LKLog
  11 +import kotlinx.coroutines.suspendCancellableCoroutine
  12 +import kotlin.coroutines.Continuation
  13 +import kotlin.coroutines.resume
  14 +
  15 +/**
  16 + * Handles connecting to a [ScreenCaptureService].
  17 + */
  18 +internal class ScreenCaptureConnection(private val context: Context) {
  19 + public var isBound = false
  20 + private set
  21 + private var service: ScreenCaptureService? = null
  22 + private val queuedConnects = mutableSetOf<Continuation<Unit>>()
  23 + private val connection: ServiceConnection = object : ServiceConnection {
  24 + override fun onServiceDisconnected(name: ComponentName) {
  25 + LKLog.v { "Screen capture service is disconnected" }
  26 + isBound = false
  27 + service = null
  28 + }
  29 +
  30 + override fun onServiceConnected(name: ComponentName, binder: IBinder) {
  31 + LKLog.v { "Screen capture service is connected" }
  32 + val screenCaptureBinder = binder as ScreenCaptureService.ScreenCaptureBinder
  33 + service = screenCaptureBinder.service
  34 + handleConnect()
  35 + }
  36 + }
  37 +
  38 + suspend fun connect() {
  39 + if (isBound) {
  40 + return
  41 + }
  42 +
  43 + val intent = Intent(context, ScreenCaptureService::class.java)
  44 + context.bindService(intent, connection, BIND_AUTO_CREATE)
  45 + return suspendCancellableCoroutine {
  46 + synchronized(this) {
  47 + if (isBound) {
  48 + it.resume(Unit)
  49 + } else {
  50 + queuedConnects.add(it)
  51 + }
  52 + }
  53 + }
  54 + }
  55 +
  56 + fun startForeground(notificationId: Int? = null, notification: Notification? = null) {
  57 + service?.start(notificationId, notification)
  58 + }
  59 +
  60 + private fun handleConnect() {
  61 + synchronized(this) {
  62 + isBound = true
  63 + queuedConnects.forEach { it.resume(Unit) }
  64 + queuedConnects.clear()
  65 + }
  66 + }
  67 +
  68 + fun stop() {
  69 + if (isBound) {
  70 + context.unbindService(connection)
  71 + }
  72 + service = null
  73 + isBound = false
  74 + }
  75 +}
  1 +package io.livekit.android.room.track.screencapture
  2 +
  3 +import android.app.Notification
  4 +import android.app.NotificationChannel
  5 +import android.app.NotificationManager
  6 +import android.app.Service
  7 +import android.content.Context
  8 +import android.content.Intent
  9 +import android.os.Binder
  10 +import android.os.Build
  11 +import android.os.IBinder
  12 +import androidx.annotation.RequiresApi
  13 +import androidx.core.app.NotificationCompat
  14 +
  15 +/**
  16 + * A foreground service is required for screen capture on API level Q (29) and up.
  17 + * This a simple default foreground service to display a notification while screen
  18 + * capturing.
  19 + */
  20 +
  21 +open class ScreenCaptureService : Service() {
  22 + private var binder: IBinder = ScreenCaptureBinder()
  23 + private var bindCount = 0
  24 + override fun onBind(intent: Intent?): IBinder {
  25 + bindCount++
  26 + return binder
  27 + }
  28 +
  29 + /**
  30 + * @param notificationId id of the notification to be used, or null for [DEFAULT_CHANNEL_ID]
  31 + * @param notification notification to be used, or null for a default notification.
  32 + */
  33 + fun start(notificationId: Int?, notification: Notification?) {
  34 + val actualNotification = if (notification != null) {
  35 + notification
  36 + } else {
  37 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
  38 + createNotificationChannel()
  39 + }
  40 + NotificationCompat.Builder(this, DEFAULT_CHANNEL_ID)
  41 + .setPriority(NotificationCompat.PRIORITY_DEFAULT)
  42 + .build()
  43 + }
  44 +
  45 + val actualId = notificationId ?: DEFAULT_NOTIFICATION_ID
  46 + startForeground(actualId, actualNotification)
  47 + }
  48 +
  49 + @RequiresApi(Build.VERSION_CODES.O)
  50 + private fun createNotificationChannel() {
  51 + val channel = NotificationChannel(
  52 + DEFAULT_CHANNEL_ID,
  53 + "Screen Capture",
  54 + NotificationManager.IMPORTANCE_LOW
  55 + )
  56 + val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
  57 + service.createNotificationChannel(channel)
  58 + }
  59 +
  60 + override fun onUnbind(intent: Intent?): Boolean {
  61 + bindCount--
  62 +
  63 + if (bindCount == 0) {
  64 + stopSelf()
  65 + }
  66 + return false
  67 + }
  68 +
  69 +
  70 + inner class ScreenCaptureBinder : Binder() {
  71 + val service: ScreenCaptureService
  72 + get() = this@ScreenCaptureService
  73 + }
  74 +
  75 + companion object {
  76 + const val DEFAULT_NOTIFICATION_ID = 2345
  77 + const val DEFAULT_CHANNEL_ID = "livekit_screen_capture"
  78 + }
  79 +}
@@ -14,6 +14,7 @@ import org.junit.Rule @@ -14,6 +14,7 @@ import org.junit.Rule
14 import org.junit.Test 14 import org.junit.Test
15 import org.junit.runner.RunWith 15 import org.junit.runner.RunWith
16 import org.mockito.Mock 16 import org.mockito.Mock
  17 +import org.mockito.Mockito
17 import org.mockito.junit.MockitoJUnit 18 import org.mockito.junit.MockitoJUnit
18 import org.robolectric.RobolectricTestRunner 19 import org.robolectric.RobolectricTestRunner
19 import org.webrtc.EglBase 20 import org.webrtc.EglBase
@@ -31,19 +32,11 @@ class RoomTest { @@ -31,19 +32,11 @@ class RoomTest {
31 @Mock 32 @Mock
32 lateinit var rtcEngine: RTCEngine 33 lateinit var rtcEngine: RTCEngine
33 34
34 - @Mock  
35 - lateinit var peerConnectionFactory: PeerConnectionFactory  
36 var eglBase: EglBase = MockEglBase() 35 var eglBase: EglBase = MockEglBase()
37 36
38 val localParticantFactory = object : LocalParticipant.Factory { 37 val localParticantFactory = object : LocalParticipant.Factory {
39 override fun create(info: LivekitModels.ParticipantInfo): LocalParticipant { 38 override fun create(info: LivekitModels.ParticipantInfo): LocalParticipant {
40 - return LocalParticipant(  
41 - info,  
42 - rtcEngine,  
43 - peerConnectionFactory,  
44 - context,  
45 - eglBase,  
46 - ) 39 + return Mockito.mock(LocalParticipant::class.java)
47 } 40 }
48 } 41 }
49 42
1 package io.livekit.android.composesample 1 package io.livekit.android.composesample
2 2
  3 +import android.app.Activity
3 import android.media.AudioManager 4 import android.media.AudioManager
  5 +import android.media.projection.MediaProjectionManager
4 import android.os.Bundle 6 import android.os.Bundle
5 import android.os.Parcelable 7 import android.os.Parcelable
6 import androidx.activity.compose.setContent 8 import androidx.activity.compose.setContent
  9 +import androidx.activity.result.contract.ActivityResultContracts
7 import androidx.appcompat.app.AppCompatActivity 10 import androidx.appcompat.app.AppCompatActivity
8 import androidx.compose.foundation.background 11 import androidx.compose.foundation.background
9 import androidx.compose.foundation.layout.* 12 import androidx.compose.foundation.layout.*
@@ -44,6 +47,19 @@ class CallActivity : AppCompatActivity() { @@ -44,6 +47,19 @@ class CallActivity : AppCompatActivity() {
44 private var previousSpeakerphoneOn = true 47 private var previousSpeakerphoneOn = true
45 private var previousMicrophoneMute = false 48 private var previousMicrophoneMute = false
46 49
  50 + private val screenCaptureIntentLauncher =
  51 + registerForActivityResult(
  52 + ActivityResultContracts.StartActivityForResult()
  53 + ) { result ->
  54 + val resultCode = result.resultCode
  55 + val data = result.data
  56 + if (resultCode != Activity.RESULT_OK || data == null) {
  57 + return@registerForActivityResult
  58 + }
  59 + viewModel.startScreenCapture(data)
  60 + }
  61 +
  62 +
47 override fun onCreate(savedInstanceState: Bundle?) { 63 override fun onCreate(savedInstanceState: Bundle?) {
48 super.onCreate(savedInstanceState) 64 super.onCreate(savedInstanceState)
49 65
@@ -73,17 +89,25 @@ class CallActivity : AppCompatActivity() { @@ -73,17 +89,25 @@ class CallActivity : AppCompatActivity() {
73 val micEnabled by viewModel.micEnabled.observeAsState(true) 89 val micEnabled by viewModel.micEnabled.observeAsState(true)
74 val videoEnabled by viewModel.videoEnabled.observeAsState(true) 90 val videoEnabled by viewModel.videoEnabled.observeAsState(true)
75 val flipButtonEnabled by viewModel.flipButtonVideoEnabled.observeAsState(true) 91 val flipButtonEnabled by viewModel.flipButtonVideoEnabled.observeAsState(true)
  92 + val screencastEnabled by viewModel.screencastEnabled.observeAsState(false)
76 Content( 93 Content(
77 room, 94 room,
78 participants, 95 participants,
79 micEnabled, 96 micEnabled,
80 videoEnabled, 97 videoEnabled,
81 - flipButtonEnabled 98 + flipButtonEnabled,
  99 + screencastEnabled,
82 ) 100 )
83 } 101 }
84 } 102 }
85 } 103 }
86 104
  105 + private fun requestMediaProjection() {
  106 + val mediaProjectionManager =
  107 + getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
  108 + screenCaptureIntentLauncher.launch(mediaProjectionManager.createScreenCaptureIntent())
  109 + }
  110 +
87 @Preview(showBackground = true, showSystemUi = true) 111 @Preview(showBackground = true, showSystemUi = true)
88 @Composable 112 @Composable
89 fun Content( 113 fun Content(
@@ -92,6 +116,7 @@ class CallActivity : AppCompatActivity() { @@ -92,6 +116,7 @@ class CallActivity : AppCompatActivity() {
92 micEnabled: Boolean = true, 116 micEnabled: Boolean = true,
93 videoEnabled: Boolean = true, 117 videoEnabled: Boolean = true,
94 flipButtonEnabled: Boolean = true, 118 flipButtonEnabled: Boolean = true,
  119 + screencastEnabled: Boolean = false,
95 ) { 120 ) {
96 ConstraintLayout( 121 ConstraintLayout(
97 modifier = Modifier 122 modifier = Modifier
@@ -224,6 +249,24 @@ class CallActivity : AppCompatActivity() { @@ -224,6 +249,24 @@ class CallActivity : AppCompatActivity() {
224 tint = Color.White, 249 tint = Color.White,
225 ) 250 )
226 } 251 }
  252 + FloatingActionButton(
  253 + onClick = {
  254 + if (!screencastEnabled) {
  255 + requestMediaProjection()
  256 + } else {
  257 + viewModel.stopScreenCapture()
  258 + }
  259 + },
  260 + backgroundColor = Color.DarkGray,
  261 + ) {
  262 + val resource =
  263 + if (screencastEnabled) R.drawable.baseline_cast_connected_24 else R.drawable.baseline_cast_24
  264 + Icon(
  265 + painterResource(id = resource),
  266 + contentDescription = "Flip Camera",
  267 + tint = Color.White,
  268 + )
  269 + }
227 } 270 }
228 } 271 }
229 } 272 }
1 package io.livekit.android.composesample 1 package io.livekit.android.composesample
2 2
3 import android.app.Application 3 import android.app.Application
  4 +import android.content.Intent
4 import androidx.lifecycle.AndroidViewModel 5 import androidx.lifecycle.AndroidViewModel
5 import androidx.lifecycle.LiveData 6 import androidx.lifecycle.LiveData
6 import androidx.lifecycle.MutableLiveData 7 import androidx.lifecycle.MutableLiveData
@@ -10,12 +11,11 @@ import io.livekit.android.ConnectOptions @@ -10,12 +11,11 @@ import io.livekit.android.ConnectOptions
10 import io.livekit.android.LiveKit 11 import io.livekit.android.LiveKit
11 import io.livekit.android.room.Room 12 import io.livekit.android.room.Room
12 import io.livekit.android.room.RoomListener 13 import io.livekit.android.room.RoomListener
  14 +import io.livekit.android.room.participant.AudioTrackPublishOptions
13 import io.livekit.android.room.participant.Participant 15 import io.livekit.android.room.participant.Participant
14 import io.livekit.android.room.participant.RemoteParticipant 16 import io.livekit.android.room.participant.RemoteParticipant
15 -import io.livekit.android.room.track.CameraPosition  
16 -import io.livekit.android.room.track.LocalAudioTrack  
17 -import io.livekit.android.room.track.LocalVideoTrack  
18 -import io.livekit.android.room.track.LocalVideoTrackOptions 17 +import io.livekit.android.room.participant.VideoTrackPublishOptions
  18 +import io.livekit.android.room.track.*
19 import kotlinx.coroutines.launch 19 import kotlinx.coroutines.launch
20 20
21 class CallViewModel( 21 class CallViewModel(
@@ -30,6 +30,7 @@ class CallViewModel( @@ -30,6 +30,7 @@ class CallViewModel(
30 30
31 private var localAudioTrack: LocalAudioTrack? = null 31 private var localAudioTrack: LocalAudioTrack? = null
32 private var localVideoTrack: LocalVideoTrack? = null 32 private var localVideoTrack: LocalVideoTrack? = null
  33 + private var localScreencastTrack: LocalScreencastVideoTrack? = null
33 34
34 private val mutableMicEnabled = MutableLiveData(true) 35 private val mutableMicEnabled = MutableLiveData(true)
35 val micEnabled = mutableMicEnabled.hide() 36 val micEnabled = mutableMicEnabled.hide()
@@ -40,6 +41,9 @@ class CallViewModel( @@ -40,6 +41,9 @@ class CallViewModel(
40 private val mutableFlipVideoButtonEnabled = MutableLiveData(true) 41 private val mutableFlipVideoButtonEnabled = MutableLiveData(true)
41 val flipButtonVideoEnabled = mutableFlipVideoButtonEnabled.hide() 42 val flipButtonVideoEnabled = mutableFlipVideoButtonEnabled.hide()
42 43
  44 + private val mutableScreencastEnabled = MutableLiveData(false)
  45 + val screencastEnabled = mutableScreencastEnabled.hide()
  46 +
43 init { 47 init {
44 viewModelScope.launch { 48 viewModelScope.launch {
45 val room = LiveKit.connect( 49 val room = LiveKit.connect(
@@ -50,14 +54,18 @@ class CallViewModel( @@ -50,14 +54,18 @@ class CallViewModel(
50 this@CallViewModel 54 this@CallViewModel
51 ) 55 )
52 56
  57 + // Create and publish audio/video tracks
53 val localParticipant = room.localParticipant 58 val localParticipant = room.localParticipant
54 val audioTrack = localParticipant.createAudioTrack() 59 val audioTrack = localParticipant.createAudioTrack()
55 - localParticipant.publishAudioTrack(audioTrack) 60 + localParticipant.publishAudioTrack(audioTrack, AudioTrackPublishOptions(dtx = true))
56 this@CallViewModel.localAudioTrack = audioTrack 61 this@CallViewModel.localAudioTrack = audioTrack
57 mutableMicEnabled.postValue(audioTrack.enabled) 62 mutableMicEnabled.postValue(audioTrack.enabled)
58 63
59 val videoTrack = localParticipant.createVideoTrack() 64 val videoTrack = localParticipant.createVideoTrack()
60 - localParticipant.publishVideoTrack(videoTrack) 65 + localParticipant.publishVideoTrack(
  66 + videoTrack,
  67 + VideoTrackPublishOptions(simulcast = false)
  68 + )
61 videoTrack.startCapture() 69 videoTrack.startCapture()
62 this@CallViewModel.localVideoTrack = videoTrack 70 this@CallViewModel.localVideoTrack = videoTrack
63 mutableVideoEnabled.postValue(videoTrack.enabled) 71 mutableVideoEnabled.postValue(videoTrack.enabled)
@@ -67,6 +75,34 @@ class CallViewModel( @@ -67,6 +75,34 @@ class CallViewModel(
67 } 75 }
68 } 76 }
69 77
  78 + fun startScreenCapture(mediaProjectionPermissionResultData: Intent) {
  79 + val localParticipant = room.value?.localParticipant ?: return
  80 + viewModelScope.launch {
  81 + val screencastTrack =
  82 + localParticipant.createScreencastTrack(mediaProjectionPermissionResultData = mediaProjectionPermissionResultData)
  83 + localParticipant.publishVideoTrack(
  84 + screencastTrack
  85 + )
  86 +
  87 + // Must start the foreground prior to startCapture.
  88 + screencastTrack.startForegroundService(null, null)
  89 + screencastTrack.startCapture()
  90 +
  91 + this@CallViewModel.localScreencastTrack = screencastTrack
  92 + mutableScreencastEnabled.postValue(screencastTrack.enabled)
  93 + }
  94 + }
  95 +
  96 + fun stopScreenCapture() {
  97 + viewModelScope.launch {
  98 + localScreencastTrack?.let { localScreencastVideoTrack ->
  99 + localScreencastVideoTrack.stop()
  100 + room.value?.localParticipant?.unpublishTrack(localScreencastVideoTrack)
  101 + mutableScreencastEnabled.postValue(localScreencastTrack?.enabled ?: false)
  102 + }
  103 + }
  104 + }
  105 +
70 private fun updateParticipants(room: Room) { 106 private fun updateParticipants(room: Room) {
71 mutableRemoteParticipants.postValue( 107 mutableRemoteParticipants.postValue(
72 room.remoteParticipants 108 room.remoteParticipants
@@ -8,6 +8,6 @@ class SampleApplication : Application() { @@ -8,6 +8,6 @@ class SampleApplication : Application() {
8 8
9 override fun onCreate() { 9 override fun onCreate() {
10 super.onCreate() 10 super.onCreate()
11 - LiveKit.loggingLevel = LoggingLevel.OFF 11 + LiveKit.loggingLevel = LoggingLevel.VERBOSE
12 } 12 }
13 } 13 }
  1 +<vector xmlns:android="http://schemas.android.com/apk/res/android"
  2 + android:width="24dp"
  3 + android:height="24dp"
  4 + android:viewportWidth="24"
  5 + android:viewportHeight="24"
  6 + android:tint="?attr/colorControlNormal">
  7 + <path
  8 + android:fillColor="@android:color/white"
  9 + android:pathData="M21,3L3,3c-1.1,0 -2,0.9 -2,2v3h2L3,5h18v14h-7v2h7c1.1,0 2,-0.9 2,-2L23,5c0,-1.1 -0.9,-2 -2,-2zM1,18v3h3c0,-1.66 -1.34,-3 -3,-3zM1,14v2c2.76,0 5,2.24 5,5h2c0,-3.87 -3.13,-7 -7,-7zM1,10v2c4.97,0 9,4.03 9,9h2c0,-6.08 -4.93,-11 -11,-11z"/>
  10 +</vector>
  1 +<vector xmlns:android="http://schemas.android.com/apk/res/android"
  2 + android:width="24dp"
  3 + android:height="24dp"
  4 + android:viewportWidth="24"
  5 + android:viewportHeight="24"
  6 + android:tint="?attr/colorControlNormal">
  7 + <path
  8 + android:fillColor="@android:color/white"
  9 + android:pathData="M1,18v3h3c0,-1.66 -1.34,-3 -3,-3zM1,14v2c2.76,0 5,2.24 5,5h2c0,-3.87 -3.13,-7 -7,-7zM19,7L5,7v1.63c3.96,1.28 7.09,4.41 8.37,8.37L19,17L19,7zM1,10v2c4.97,0 9,4.03 9,9h2c0,-6.08 -4.93,-11 -11,-11zM21,3L3,3c-1.1,0 -2,0.9 -2,2v3h2L3,5h18v14h-7v2h7c1.1,0 2,-0.9 2,-2L23,5c0,-1.1 -0.9,-2 -2,-2z"/>
  10 +</vector>