Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detect screenshare rotation #552

Merged
merged 6 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/nice-clocks-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"client-sdk-android": minor
---

Detect rotation for screenshare tracks
5 changes: 5 additions & 0 deletions .changeset/stale-ways-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"client-sdk-android": patch
---

Update Kotlin dependency to 1.9.25
5 changes: 5 additions & 0 deletions .changeset/thirty-readers-lie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"client-sdk-android": minor
---

Add separate default capture/publish options for screenshare tracks
2 changes: 1 addition & 1 deletion .idea/kotlinc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions deps.gradle
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
ext {
android_build_tools_version = '8.2.2'
android_build_tools_version = '8.7.2'
compose_version = '1.2.1'
compose_compiler_version = '1.4.5'
kotlin_version = '1.8.20'
compose_compiler_version = '1.5.15'
kotlin_version = '1.9.25'
java_version = JavaVersion.VERSION_1_8
dokka_version = '1.8.20'
dokka_version = '1.9.20'
androidSdk = [
compileVersion: 34,
targetVersion : 34,
compileVersion: 35,
targetVersion : 35,
minVersion : 21,
]
generated = [
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version = "4.1.
#noinspection GradleDependency
mockito-inline = { module = "org.mockito:mockito-inline", version = "4.11.0" }

robolectric = { module = "org.robolectric:robolectric", version = "4.11.1" }
robolectric = { module = "org.robolectric:robolectric", version = "4.14.1" }
turbine = { module = "app.cash.turbine:turbine", version = "1.0.0" }
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#Mon May 01 22:58:53 JST 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
5 changes: 3 additions & 2 deletions livekit-android-sdk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ android {
targetCompatibility java_version
}
packagingOptions {
// Exclude our protos from being included in the final aar.
exclude "**/*.proto"
resources {
excludes += ['**/*.proto']
}
}

buildFeatures {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,6 @@ data class RoomOptions(
val videoTrackCaptureDefaults: LocalVideoTrackOptions? = null,
val audioTrackPublishDefaults: AudioTrackPublishDefaults? = null,
val videoTrackPublishDefaults: VideoTrackPublishDefaults? = null,
val screenShareTrackCaptureDefaults: LocalVideoTrackOptions? = null,
val screenShareTrackPublishDefaults: VideoTrackPublishDefaults? = null,
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023 LiveKit, Inc.
* Copyright 2023-2024 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -20,6 +20,7 @@ import io.livekit.android.room.participant.AudioTrackPublishDefaults
import io.livekit.android.room.participant.VideoTrackPublishDefaults
import io.livekit.android.room.track.LocalAudioTrackOptions
import io.livekit.android.room.track.LocalVideoTrackOptions
import io.livekit.android.room.track.ScreenSharePresets
import javax.inject.Inject
import javax.inject.Singleton

Expand All @@ -31,4 +32,6 @@ constructor() {
var audioTrackPublishDefaults: AudioTrackPublishDefaults = AudioTrackPublishDefaults()
var videoTrackCaptureDefaults: LocalVideoTrackOptions = LocalVideoTrackOptions()
var videoTrackPublishDefaults: VideoTrackPublishDefaults = VideoTrackPublishDefaults()
var screenShareTrackCaptureDefaults: LocalVideoTrackOptions = LocalVideoTrackOptions(isScreencast = true, captureParams = ScreenSharePresets.ORIGINAL.capture)
var screenShareTrackPublishDefaults: VideoTrackPublishDefaults = VideoTrackPublishDefaults(videoEncoding = ScreenSharePresets.ORIGINAL.encoding)
}
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,16 @@ constructor(
*/
var videoTrackPublishDefaults: VideoTrackPublishDefaults by defaultsManager::videoTrackPublishDefaults

/**
* Default options to use when creating a screen share track.
*/
var screenShareTrackCaptureDefaults: LocalVideoTrackOptions by defaultsManager::screenShareTrackCaptureDefaults

/**
* Default options to use when publishing a screen share track.
*/
var screenShareTrackPublishDefaults: VideoTrackPublishDefaults by defaultsManager::screenShareTrackPublishDefaults

val localParticipant: LocalParticipant = localParticipantFactory.create(dynacast = false).apply {
internalListener = this@Room
}
Expand Down Expand Up @@ -285,11 +295,13 @@ constructor(
RoomOptions(
adaptiveStream = adaptiveStream,
dynacast = dynacast,
e2eeOptions = e2eeOptions,
audioTrackCaptureDefaults = audioTrackCaptureDefaults,
videoTrackCaptureDefaults = videoTrackCaptureDefaults,
audioTrackPublishDefaults = audioTrackPublishDefaults,
videoTrackPublishDefaults = videoTrackPublishDefaults,
e2eeOptions = e2eeOptions,
screenShareTrackCaptureDefaults = screenShareTrackCaptureDefaults,
screenShareTrackPublishDefaults = screenShareTrackPublishDefaults,
)

/**
Expand Down Expand Up @@ -502,6 +514,12 @@ constructor(
options.videoTrackPublishDefaults?.let {
videoTrackPublishDefaults = it
}
options.screenShareTrackCaptureDefaults?.let {
screenShareTrackCaptureDefaults = it
}
options.screenShareTrackPublishDefaults?.let {
screenShareTrackPublishDefaults = it
}
adaptiveStream = options.adaptiveStream
dynacast = options.dynacast
e2eeOptions = options.e2eeOptions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ internal constructor(
var audioTrackPublishDefaults: AudioTrackPublishDefaults by defaultsManager::audioTrackPublishDefaults
var videoTrackCaptureDefaults: LocalVideoTrackOptions by defaultsManager::videoTrackCaptureDefaults
var videoTrackPublishDefaults: VideoTrackPublishDefaults by defaultsManager::videoTrackPublishDefaults
var screenShareTrackCaptureDefaults: LocalVideoTrackOptions by defaultsManager::screenShareTrackCaptureDefaults
var screenShareTrackPublishDefaults: VideoTrackPublishDefaults by defaultsManager::screenShareTrackPublishDefaults

private var republishes: List<LocalTrackPublication>? = null
private val localTrackPublications
Expand Down Expand Up @@ -181,13 +183,13 @@ internal constructor(
* @param name The name of the track.
* @param mediaProjectionPermissionResultData The resultData returned from launching
* [MediaProjectionManager.createScreenCaptureIntent()](https://developer.android.com/reference/android/media/projection/MediaProjectionManager#createScreenCaptureIntent()).
* @param options The capture options to use for this track, or [Room.videoTrackCaptureDefaults] if none is passed.
* @param options The capture options to use for this track, or [Room.screenShareTrackCaptureDefaults] if none is passed.
* @param videoProcessor A video processor to attach to this track that can modify the frames before publishing.
*/
fun createScreencastTrack(
name: String = "",
mediaProjectionPermissionResultData: Intent,
options: LocalVideoTrackOptions = videoTrackCaptureDefaults.copy(),
options: LocalVideoTrackOptions = screenShareTrackCaptureDefaults.copy(),
videoProcessor: VideoProcessor? = null,
): LocalScreencastVideoTrack {
val screencastOptions = options.copy(isScreencast = true)
Expand Down Expand Up @@ -249,8 +251,8 @@ internal constructor(
* @param mediaProjectionPermissionResultData The resultData returned from launching
* [MediaProjectionManager.createScreenCaptureIntent()](https://developer.android.com/reference/android/media/projection/MediaProjectionManager#createScreenCaptureIntent()).
* @throws IllegalArgumentException if attempting to enable screenshare without [mediaProjectionPermissionResultData]
* @see Room.videoTrackCaptureDefaults
* @see Room.videoTrackPublishDefaults
* @see Room.screenShareTrackCaptureDefaults
* @see Room.screenShareTrackPublishDefaults
*/
suspend fun setScreenShareEnabled(
enabled: Boolean,
Expand Down Expand Up @@ -294,7 +296,7 @@ internal constructor(
createScreencastTrack(mediaProjectionPermissionResultData = mediaProjectionPermissionResultData)
track.startForegroundService(null, null)
track.startCapture()
publishVideoTrack(track)
publishVideoTrack(track, options = VideoTrackPublishOptions(null, screenShareTrackPublishDefaults))
}

else -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,35 @@ import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.media.projection.MediaProjection
import android.util.DisplayMetrics
import android.view.OrientationEventListener
import android.view.WindowManager
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.livekit.android.room.DefaultsManager
import io.livekit.android.room.participant.LocalParticipant
import io.livekit.android.room.track.screencapture.ScreenCaptureConnection
import io.livekit.android.room.track.screencapture.ScreenCaptureService
import livekit.org.webrtc.*
import java.util.*
import io.livekit.android.util.LKLog
import livekit.org.webrtc.EglBase
import livekit.org.webrtc.PeerConnectionFactory
import livekit.org.webrtc.ScreenCapturerAndroid
import livekit.org.webrtc.SurfaceTextureHelper
import livekit.org.webrtc.VideoCapturer
import livekit.org.webrtc.VideoProcessor
import livekit.org.webrtc.VideoSource
import java.util.UUID

/**
* A video track that captures the screen for publishing.
*
* Note: A foreground service is generally required for use. Use [startForegroundService] or start
* your own foreground service before starting the video track.
*
* @see LocalParticipant.createScreencastTrack
* @see LocalScreencastVideoTrack.startForegroundService
*/
class LocalScreencastVideoTrack
@AssistedInject
constructor(
Expand Down Expand Up @@ -58,6 +78,76 @@ constructor(
videoTrackFactory,
) {

private var prevDisplayWidth = 0
private var prevDisplayHeight = 0
private val displayMetrics = DisplayMetrics()
private val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
private val orientationEventListener = object : OrientationEventListener(context) {
override fun onOrientationChanged(orientation: Int) {
if (isDisposed) {
this.disable()
return
}
updateCaptureFormatIfNeeded()
}
}

private fun getCaptureDimensions(displayWidth: Int, displayHeight: Int): Pair<Int, Int> {
val captureWidth: Int
val captureHeight: Int

if (options.captureParams.width == 0 && options.captureParams.height == 0) {
// Use raw display size
captureWidth = displayWidth
captureHeight = displayHeight
} else {
// Use captureParams.width as longest side and captureParams.height as shortest side.
if (displayWidth > displayHeight) {
captureWidth = options.captureParams.width
captureHeight = options.captureParams.height
} else {
captureWidth = options.captureParams.height
captureHeight = options.captureParams.width
}
}

return captureWidth to captureHeight
}

private fun updateCaptureFormatIfNeeded() {
windowManager.defaultDisplay.getRealMetrics(displayMetrics)
val displayWidth = displayMetrics.widthPixels
val displayHeight = displayMetrics.heightPixels

// Updates whenever the display rotates
if (displayWidth != prevDisplayWidth || displayHeight != prevDisplayHeight) {
prevDisplayWidth = displayWidth
prevDisplayHeight = displayHeight

val (captureWidth, captureHeight) = getCaptureDimensions(displayWidth, displayHeight)

try {
capturer.changeCaptureFormat(captureWidth, captureHeight, options.captureParams.maxFps)
} catch (e: Exception) {
LKLog.w(e) { "Exception when changing capture format of the screen share track." }
}
}
}

override fun startCapture() {
// Don't use super.startCapture, must calculate correct dimensions
windowManager.defaultDisplay.getRealMetrics(displayMetrics)
val displayWidth = displayMetrics.widthPixels
val displayHeight = displayMetrics.heightPixels
val (captureWidth, captureHeight) = getCaptureDimensions(displayWidth, displayHeight)

capturer.startCapture(captureWidth, captureHeight, options.captureParams.maxFps)

if (orientationEventListener.canDetectOrientation()) {
orientationEventListener.enable()
}
}

private val serviceConnection = ScreenCaptureConnection(context)

init {
Expand Down Expand Up @@ -95,6 +185,7 @@ constructor(
override fun stop() {
super.stop()
serviceConnection.stop()
orientationEventListener.disable()
}

@AssistedFactory
Expand Down Expand Up @@ -129,7 +220,7 @@ constructor(
options: LocalVideoTrackOptions,
rootEglBase: EglBase,
screencastVideoTrackFactory: Factory,
videoProcessor: VideoProcessor?
videoProcessor: VideoProcessor?,
): LocalScreencastVideoTrack {
val source = peerConnectionFactory.createVideoSource(options.isScreencast)
source.setVideoProcessor(videoProcessor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,48 @@ enum class VideoPreset43(
VideoEncoding(3_800_000, 30),
),
}

/**
* 16:9 Video presets along with suggested bitrates.
*/
enum class ScreenSharePresets(
override val capture: VideoCaptureParameter,
override val encoding: VideoEncoding,
) : VideoPreset {
H360_FPS3(
VideoCaptureParameter(640, 360, 3),
VideoEncoding(200_000, 3),
),
H360_FPS15(
VideoCaptureParameter(640, 360, 15),
VideoEncoding(400_000, 15),
),
H720_FPS5(
VideoCaptureParameter(1280, 720, 5),
VideoEncoding(800_000, 5),
),
H720_FPS15(
VideoCaptureParameter(1280, 720, 15),
VideoEncoding(1_500_000, 15),
),
H720_FPS30(
VideoCaptureParameter(1280, 720, 30),
VideoEncoding(2_000_000, 30),
),
H1080_FPS15(
VideoCaptureParameter(1920, 1080, 15),
VideoEncoding(2_500_000, 15),
),
H1080_FPS30(
VideoCaptureParameter(1920, 1080, 30),
VideoEncoding(5_000_000, 30),
),

/**
* Uses the original resolution without resizing.
*/
ORIGINAL(
VideoCaptureParameter(0, 0, 30),
VideoEncoding(7_000_000, 30),
)
}
6 changes: 3 additions & 3 deletions livekit-android-test/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@ android {
consumerProguardFiles "consumer-rules.pro"
}

lintOptions {
disable 'VisibleForTests'
}
buildTypes {
release {
minifyEnabled false
Expand All @@ -39,6 +36,9 @@ android {
includeAndroidResources = true
}
}
lint {
disable 'VisibleForTests'
}
}

dokkaHtml {
Expand Down
Loading
Loading