davidliu
Committed by GitHub

Changes for compose components livekit library (#282)

* save work

* update deps

* update deps

* Fix build

* Fix github actions

* Spotless fixes
正在显示 39 个修改的文件 包含 640 行增加181 行删除
... ... @@ -32,7 +32,7 @@ jobs:
- name: set up JDK 12
uses: actions/setup-java@v3.12.0
with:
java-version: '12'
java-version: '17'
distribution: 'adopt'
- uses: actions/cache@v3.3.2
... ... @@ -161,12 +161,14 @@ jobs:
sed -i -e "s,signing.keyId=,signing.keyId=$GPG_KEY_ID,g" gradle.properties
sed -i -e "s,signing.password=,signing.password=$GPG_PASSWORD,g" gradle.properties
sed -i -e "s,signing.secretKeyRingFile=,signing.secretKeyRingFile=$GITHUB_WORKSPACE/release.gpg,g" gradle.properties
sed -i -e "s,STAGING_PROFILE_ID=,STAGING_PROFILE_ID=$PROFILE_ID,g" gradle.properties
env:
GPG_KEY_ARMOR: "${{ secrets.SIGNING_KEY_ARMOR }}"
GPG_KEY_ID: ${{ secrets.SIGNING_KEY_ID }}
GPG_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
NEXUS_USERNAME: ${{ secrets.NEXUS_USERNAME }}
NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }}
PROFILE_ID: ${{ secrets.STAGING_PROFILE_ID }}
- name: Publish snapshot
if: github.event_name == 'push' && contains(steps.version_name.outputs.version_name,'SNAPSHOT')
... ...
... ... @@ -15,15 +15,15 @@ jobs:
working-directory: ./client-sdk-android
steps:
- name: checkout client-sdk-android
uses: actions/checkout@v2.3.4
uses: actions/checkout@v4.0.0
with:
path: ./client-sdk-android
submodules: recursive
- name: set up JDK 12
uses: actions/setup-java@v2
uses: actions/setup-java@v3.12.0
with:
java-version: '12'
java-version: '17'
distribution: 'adopt'
- name: Grant execute permission for gradlew
... ... @@ -51,15 +51,17 @@ jobs:
sed -i -e "s,signing.keyId=,signing.keyId=$GPG_KEY_ID,g" gradle.properties
sed -i -e "s,signing.password=,signing.password=$GPG_PASSWORD,g" gradle.properties
sed -i -e "s,signing.secretKeyRingFile=,signing.secretKeyRingFile=$GITHUB_WORKSPACE/release.gpg,g" gradle.properties
sed -i -e "s,STAGING_PROFILE_ID=,STAGING_PROFILE_ID=$PROFILE_ID,g" gradle.properties
env:
GPG_KEY_ARMOR: "${{ secrets.SIGNING_KEY_ARMOR }}"
GPG_KEY_ID: ${{ secrets.SIGNING_KEY_ID }}
GPG_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
NEXUS_USERNAME: ${{ secrets.NEXUS_USERNAME }}
NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }}
PROFILE_ID: ${{ secrets.STAGING_PROFILE_ID }}
- name: Publish to sonatype
run: ./gradlew publish
- name: Close and release to maven
run: ./gradlew closeAndReleaseRepository
... ...
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="1.8" />
<bytecodeTargetLevel target="17" />
</component>
</project>
\ No newline at end of file
... ...
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.7.10" />
<option name="version" value="1.8.20" />
</component>
</project>
\ No newline at end of file
... ...
... ... @@ -8,11 +8,11 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.2.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10"
classpath "com.android.tools.build:gradle:$android_build_tools_version"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version"
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.19'
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.9.3'
classpath "io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.30.0"
classpath 'com.dicedmelon.gradle:jacoco-android:0.1.5'
classpath "com.diffplug.spotless:spotless-plugin-gradle:6.21.0"
... ... @@ -72,7 +72,7 @@ task clean(type: Delete) {
nexusStaging {
serverUrl = "https://s01.oss.sonatype.org/service/local/"
packageGroup = GROUP
stagingProfileId = "16b57cbf143daa"
stagingProfileId = STAGING_PROFILE_ID
}
afterEvaluate {
... ...
ext {
compose_version = '1.2.1'
compose_compiler_version = '1.3.0'
kotlin_version = '1.7.10'
java_version = JavaVersion.VERSION_1_8
android_build_tools_version = '8.0.2'
compose_version = '1.4.2'
compose_compiler_version = '1.4.6'
kotlin_version = '1.8.20'
java_version = JavaVersion.VERSION_17
dokka_version = '1.5.0'
androidSdk = [
compileVersion: 33,
... ... @@ -10,21 +11,27 @@ ext {
minVersion : 21,
]
versions = [
androidx_core : "1.8.0",
androidx_core : "1.10.1",
androidx_lifecycle: "2.5.1",
autoService : '1.0.1',
dagger : "2.43",
coroutines : "1.6.0",
dagger : "2.46",
groupie : "2.9.0",
junit : "4.13.2",
junitJupiter : "5.5.0",
coroutines : "1.6.0",
lint : "30.0.1",
serialization : "1.5.0",
protobuf : "3.22.0",
]
generated = [
protoSrc: "$projectDir/protocol",
]
deps = [
androidx : [
'annotation' : 'androidx.annotation:annotation:1.6.0',
'activity_compose' : 'androidx.activity:activity-compose:1.7.1',
'constraintlayout_compose': "androidx.constraintlayout:constraintlayout-compose:1.0.1",
],
auto : [
'service' : "com.google.auto.service:auto-service:${versions.autoService}",
'serviceAnnotations': "com.google.auto.service:auto-service-annotations:${versions.autoService}",
... ... @@ -33,7 +40,11 @@ ext {
"lib" : "org.jetbrains.kotlinx:kotlinx-coroutines-android:${versions.coroutines}",
"test": "org.jetbrains.kotlinx:kotlinx-coroutines-test: ${versions.coroutines}",
],
compose : [
"bom": "androidx.compose:compose-bom:2023.04.01",
],
timber : "com.github.ajalt:timberkt:1.5.1",
// lint
lint : "com.android.tools.lint:lint:${versions.lint}",
lintApi : "com.android.tools.lint:lint-api:${versions.lint}",
... ... @@ -41,9 +52,20 @@ ext {
lintTests : "com.android.tools.lint:lint-tests:${versions.lint}",
// tests
androidx_test : [
"core" : 'androidx.test:core:1.5.0',
"junit": "androidx.test.ext:junit:1.1.5",
],
espresso : 'androidx.test.espresso:espresso-core:3.5.1',
junit : "junit:junit:${versions.junit}",
junitJupiterApi : "org.junit.jupiter:junit-jupiter-api:${versions.junitJupiter}",
junitJupiterEngine: "org.junit.jupiter:junit-jupiter-engine:${versions.junitJupiter}",
mockito : [
"core" : 'org.mockito:mockito-core:4.0.0',
"kotlin": "org.mockito.kotlin:mockito-kotlin:4.0.0",
],
robolectric : 'org.robolectric:robolectric:4.10.2',
]
annotations = [
]
... ...
... ... @@ -51,6 +51,8 @@ signing.keyId=
signing.password=
signing.secretKeyRingFile=
STAGING_PROFILE_ID=
RELEASE_SIGNING_ENABLED=true
# For instrumented tests.
... ... @@ -58,4 +60,4 @@ RELEASE_SIGNING_ENABLED=true
# Instead, override in ~/.gradle/gradle.properties
livekitUrl=
livekitApiKey=
livekitApiSecret=
\ No newline at end of file
livekitApiSecret=
... ...
... ... @@ -105,12 +105,12 @@ afterEvaluate { project ->
}
task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) {
classifier = 'javadoc'
archiveClassifier = 'javadoc'
from androidJavadocs.destinationDir
}
task androidSourcesJar(type: Jar) {
classifier = 'sources'
archiveClassifier = 'sources'
from android.sourceSets.main.java.source
}
}
... ... @@ -131,13 +131,13 @@ afterEvaluate { project ->
}
}
artifacts {
if (project.getPlugins().hasPlugin('com.android.application') ||
project.getPlugins().hasPlugin('com.android.library')) {
archives androidSourcesJar
archives androidJavadocsJar
}
}
// artifacts {
// if (project.getPlugins().hasPlugin('com.android.application') ||
// project.getPlugins().hasPlugin('com.android.library')) {
// archives androidSourcesJar
// archives androidJavadocsJar
// }
// }
android.libraryVariants.all { variant ->
tasks.androidJavadocs.doFirst {
... ... @@ -149,8 +149,8 @@ afterEvaluate { project ->
publication.groupId = GROUP
publication.version = VERSION_NAME
publication.artifact androidSourcesJar
publication.artifact androidJavadocsJar
// publication.artifact androidSourcesJar
// publication.artifact androidJavadocsJar
configurePom(publication.pom)
}
... ... @@ -162,4 +162,4 @@ afterEvaluate { project ->
}
}
}
}
\ No newline at end of file
}
... ...
#Mon May 22 01:18:06 JST 2023
#Mon May 01 22:58:53 JST 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
... ...
# Android Kotlin SDK for LiveKit
----
... ...
... ... @@ -6,13 +6,12 @@ plugins {
id 'kotlinx-serialization'
id 'com.google.protobuf'
id 'jacoco'
id 'com.dicedmelon.gradle.jacoco-android'
id("com.mxalbert.gradle.jacoco-android") version "0.2.1"
}
android {
namespace 'io.livekit.android'
compileSdkVersion androidSdk.compileVersion
buildToolsVersion "30.0.3"
defaultConfig {
minSdkVersion androidSdk.minVersion
... ... @@ -58,15 +57,20 @@ android {
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion compose_compiler_version
buildConfig = true
}
kotlinOptions {
freeCompilerArgs = ["-Xinline-classes", "-opt-in=kotlin.RequiresOptIn"]
jvmTarget = java_version
}
publishing {
singleVariant("release") {
withJavadocJar()
withSourcesJar()
}
}
}
protobuf {
... ... @@ -100,7 +104,7 @@ tasks.withType(Test) {
}
jacocoAndroidUnitTestReport {
excludes += ['livekit/**',]
excludes.add('livekit/**')
}
dokkaHtml {
... ... @@ -116,7 +120,7 @@ dokkaHtml {
// URL showing where the source code can be accessed through the web browser
remoteUrl.set(new URL(
"https://github.com/livekit/client-sdk-android/tree/master/livekit-android-sdk/src/main/java"))
"https://github.com/livekit/client-sdk-android/tree/master/livekit-android-sdk/src/main/java"))
// Suffix which is used to append the line number to the URL. Use #L for GitHub
remoteLineSuffix.set("#L")
}
... ... @@ -138,14 +142,13 @@ dependencies {
//api fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation deps.coroutines.lib
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:${versions.serialization}"
api 'io.github.webrtc-sdk:android:114.5735.05'
api "com.squareup.okhttp3:okhttp:4.10.0"
api 'com.github.davidliu:audioswitch:d18e3e31d427c27f1593030e024b370bf24480fd'
implementation "androidx.annotation:annotation:1.4.0"
implementation deps.androidx.annotation
implementation "androidx.core:core:${versions.androidx_core}"
implementation "com.google.protobuf:protobuf-javalite:${versions.protobuf}"
implementation "androidx.compose.ui:ui:$compose_version"
implementation "com.google.dagger:dagger:${versions.dagger}"
kapt "com.google.dagger:dagger-compiler:${versions.dagger}"
... ... @@ -156,15 +159,15 @@ dependencies {
lintChecks project(':livekit-lint')
lintPublish project(':livekit-lint')
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.robolectric:robolectric:4.10.2'
testImplementation 'org.mockito:mockito-core:4.0.0'
testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0"
testImplementation 'androidx.test:core:1.4.0'
testImplementation deps.junit
testImplementation deps.robolectric
testImplementation deps.mockito.core
testImplementation deps.mockito.kotlin
testImplementation deps.androidx_test.core
testImplementation deps.coroutines.test
kaptTest "com.google.dagger:dagger-compiler:${versions.dagger}"
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation deps.androidx_test.junit
androidTestImplementation deps.espresso
}
apply from: rootProject.file('gradle/gradle-mvn-push.gradle')
... ...
... ... @@ -14,8 +14,7 @@
limitations under the License.
-->
<manifest package="io.livekit.android"
xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
... ...
... ... @@ -19,7 +19,6 @@ package io.livekit.android.room.track
import android.view.View
import io.livekit.android.dagger.InjectionNames
import io.livekit.android.events.TrackEvent
import io.livekit.android.room.track.video.ComposeVisibility
import io.livekit.android.room.track.video.VideoSinkVisibility
import io.livekit.android.room.track.video.ViewVisibility
import io.livekit.android.util.LKLog
... ... @@ -55,7 +54,7 @@ class RemoteVideoTrack(
/**
* If `autoManageVideo` is enabled, a VideoSinkVisibility should be passed, using
* [ViewVisibility] if using a traditional View layout, or [ComposeVisibility]
* [ViewVisibility] if using a traditional View layout, or ComposeVisibility
* if using Jetpack Compose.
*
* By default, any Views passed to this method will be added with a [ViewVisibility].
... ...
... ... @@ -22,10 +22,9 @@ import android.os.Looper
import android.view.View
import android.view.ViewTreeObserver
import androidx.annotation.CallSuper
import androidx.compose.ui.layout.LayoutCoordinates
import io.livekit.android.room.track.Track
import io.livekit.android.room.track.video.ViewVisibility.Notifier
import java.util.*
import java.util.Observable
abstract class VideoSinkVisibility : Observable() {
abstract fun isVisible(): Boolean
... ... @@ -48,46 +47,6 @@ abstract class VideoSinkVisibility : Observable() {
}
}
class ComposeVisibility : VideoSinkVisibility() {
private var coordinates: LayoutCoordinates? = null
private var lastVisible = isVisible()
private var lastSize = size()
override fun isVisible(): Boolean {
return (coordinates?.isAttached == true &&
coordinates?.size?.width != 0 &&
coordinates?.size?.height != 0)
}
override fun size(): Track.Dimensions {
val width = coordinates?.size?.width ?: 0
val height = coordinates?.size?.height ?: 0
return Track.Dimensions(width, height)
}
// Note, LayoutCoordinates are mutable and may be reused.
fun onGloballyPositioned(layoutCoordinates: LayoutCoordinates) {
coordinates = layoutCoordinates
val visible = isVisible()
val size = size()
if (lastVisible != visible || lastSize != size) {
notifyChanged()
}
lastVisible = visible
lastSize = size
}
fun onDispose() {
if (coordinates == null) {
return
}
coordinates = null
notifyChanged()
}
}
/**
* A [VideoSinkVisibility] for views. If using a custom view other than the sdk provided renderers,
* you must implement [Notifier], override [View.onVisibilityChanged] and call through to [recalculate], or
... ... @@ -102,7 +61,6 @@ class ViewVisibility(private val view: View) : VideoSinkVisibility() {
private val globalLayoutListener = ViewTreeObserver.OnGlobalLayoutListener {
scheduleRecalculate()
}
private val scrollListener = ViewTreeObserver.OnScrollChangedListener {
scheduleRecalculate()
}
... ...
... ... @@ -36,7 +36,11 @@ limitations under the License.
Original repo can be found at: https://github.com/ajalt/LKLogkt
*/
internal class LKLog {
/**
* @suppress
*/
@Suppress("NOTHING_TO_INLINE", "unused")
class LKLog {
companion object {
var loggingLevel = OFF
... ... @@ -90,7 +94,7 @@ internal class LKLog {
inline fun wtf(t: Throwable?) = log(WTF) { Timber.wtf(t) }
/** @suppress */
internal inline fun log(loggingLevel: LoggingLevel, block: () -> Unit) {
inline fun log(loggingLevel: LoggingLevel, block: () -> Unit) {
if (loggingLevel >= LKLog.loggingLevel && Timber.treeCount() > 0) block()
}
}
... ...
... ... @@ -159,6 +159,7 @@ class MockPeerConnection(
return super.addTransceiver(mediaType, init)
}
@Deprecated("Deprecated in Java")
override fun getStats(observer: StatsObserver?, track: MediaStreamTrack?): Boolean {
observer?.onComplete(emptyArray())
return true
... ...
... ... @@ -5,8 +5,8 @@ plugins {
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = java_version
targetCompatibility = java_version
}
dependencies {
... ...
... ... @@ -4,12 +4,13 @@ plugins {
}
android {
compileSdk 32
namespace 'io.livekit.android.sample.basic'
compileSdk 33
defaultConfig {
applicationId "io.livekit.android.sample.basic"
minSdk 21
targetSdk 32
targetSdk 33
versionCode 1
versionName "1.0"
... ... @@ -23,11 +24,11 @@ android {
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility java_version
targetCompatibility java_version
}
kotlinOptions {
jvmTarget = '1.8'
jvmTarget = java_version
}
}
... ...
... ... @@ -15,6 +15,7 @@ final url = getDefaultUrl()
final token = getDefaultToken()
android {
namespace "io.livekit.android.sample.common"
compileSdk androidSdk.compileVersion
defaultConfig {
... ... @@ -39,12 +40,15 @@ android {
buildConfigField "String", "DEFAULT_TOKEN", "\"$token\""
}
}
buildFeatures {
buildConfig = true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility java_version
targetCompatibility java_version
}
kotlinOptions {
jvmTarget = '1.8'
jvmTarget = java_version
}
}
... ...
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.livekit.android.sample.common">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
... ...
... ... @@ -111,6 +111,8 @@ class CallViewModel(
private val mutablePermissionAllowed = MutableStateFlow(true)
val permissionAllowed = mutablePermissionAllowed.hide()
var messagesReceived = 0
init {
viewModelScope.launch {
// Collect any errors.
... ... @@ -137,14 +139,9 @@ class CallViewModel(
is RoomEvent.FailedToConnect -> mutableError.value = it.error
is RoomEvent.DataReceived -> {
val identity = it.participant?.identity ?: "server"
val message = it.data.toString(Charsets.UTF_8)
mutableDataReceived.emit("$identity: $message")
}
is RoomEvent.TrackSubscribed -> {
launch { collectTrackStats(it) }
messagesReceived++
Timber.e { "message received from $identity, count $messagesReceived" }
}
else -> {
Timber.e { "Room event: $it" }
}
... ...
... ... @@ -5,6 +5,7 @@ plugins {
}
android {
namespace 'io.livekit.android.composesample'
compileSdkVersion androidSdk.compileVersion
defaultConfig {
... ...
/*
* Copyright 2023 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.composesample
import android.app.Activity
... ... @@ -31,6 +47,7 @@ import io.livekit.android.composesample.ui.theme.AppTheme
import io.livekit.android.room.Room
import io.livekit.android.room.participant.Participant
import io.livekit.android.sample.CallViewModel
import io.livekit.android.sample.common.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
... ... @@ -45,13 +62,13 @@ class CallActivity : AppCompatActivity() {
token = args.token,
e2ee = args.e2eeOn,
e2eeKey = args.e2eeKey,
application = application
application = application,
)
}
private val screenCaptureIntentLauncher =
registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
ActivityResultContracts.StartActivityForResult(),
) { result ->
val resultCode = result.resultCode
val data = result.data
... ... @@ -146,24 +163,26 @@ class CallActivity : AppCompatActivity() {
ConstraintLayout(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.background)
.background(MaterialTheme.colors.background),
) {
val (speakerView, audienceRow, buttonBar) = createRefs()
// Primary speaker view
Surface(modifier = Modifier.constrainAs(speakerView) {
top.linkTo(parent.top)
start.linkTo(parent.start)
end.linkTo(parent.end)
bottom.linkTo(audienceRow.top)
width = Dimension.fillToConstraints
height = Dimension.fillToConstraints
}) {
Surface(
modifier = Modifier.constrainAs(speakerView) {
top.linkTo(parent.top)
start.linkTo(parent.start)
end.linkTo(parent.end)
bottom.linkTo(audienceRow.top)
width = Dimension.fillToConstraints
height = Dimension.fillToConstraints
},
) {
if (room != null && primarySpeaker != null) {
ParticipantItem(
room = room,
participant = primarySpeaker,
isSpeaking = activeSpeakers.contains(primarySpeaker)
isSpeaking = activeSpeakers.contains(primarySpeaker),
)
}
}
... ... @@ -178,12 +197,12 @@ class CallActivity : AppCompatActivity() {
end.linkTo(parent.end)
width = Dimension.fillToConstraints
height = Dimension.value(120.dp)
}
},
) {
if (room != null) {
items(
count = participants.size,
key = { index -> participants[index].sid }
key = { index -> participants[index].sid },
) { index ->
ParticipantItem(
room = room,
... ... @@ -191,7 +210,7 @@ class CallActivity : AppCompatActivity() {
isSpeaking = activeSpeakers.contains(participants[index]),
modifier = Modifier
.fillMaxHeight()
.aspectRatio(1.0f, true)
.aspectRatio(1.0f, true),
)
}
}
... ... @@ -208,7 +227,7 @@ class CallActivity : AppCompatActivity() {
height = Dimension.wrapContent
},
verticalArrangement = Arrangement.SpaceEvenly,
horizontalAlignment = Alignment.CenterHorizontally
horizontalAlignment = Alignment.CenterHorizontally,
) {
val controlSize = 40.dp
val controlPadding = 4.dp
... ... @@ -221,7 +240,7 @@ class CallActivity : AppCompatActivity() {
onClick = { viewModel.setMicEnabled(!micEnabled) },
modifier = Modifier
.size(controlSize)
.padding(controlPadding)
.padding(controlPadding),
) {
val resource =
if (micEnabled) R.drawable.outline_mic_24 else R.drawable.outline_mic_off_24
... ... @@ -235,7 +254,7 @@ class CallActivity : AppCompatActivity() {
onClick = { viewModel.setCameraEnabled(!videoEnabled) },
modifier = Modifier
.size(controlSize)
.padding(controlPadding)
.padding(controlPadding),
) {
val resource =
if (videoEnabled) R.drawable.outline_videocam_24 else R.drawable.outline_videocam_off_24
... ... @@ -249,7 +268,7 @@ class CallActivity : AppCompatActivity() {
onClick = { viewModel.flipCamera() },
modifier = Modifier
.size(controlSize)
.padding(controlPadding)
.padding(controlPadding),
) {
Icon(
painterResource(id = R.drawable.outline_flip_camera_android_24),
... ... @@ -267,7 +286,7 @@ class CallActivity : AppCompatActivity() {
},
modifier = Modifier
.size(controlSize)
.padding(controlPadding)
.padding(controlPadding),
) {
val resource =
if (screencastEnabled) R.drawable.baseline_cast_connected_24 else R.drawable.baseline_cast_24
... ... @@ -284,7 +303,7 @@ class CallActivity : AppCompatActivity() {
onClick = { showMessageDialog = true },
modifier = Modifier
.size(controlSize)
.padding(controlPadding)
.padding(controlPadding),
) {
Icon(
painterResource(id = R.drawable.baseline_chat_24),
... ... @@ -316,7 +335,7 @@ class CallActivity : AppCompatActivity() {
onSendMessage(messageToSend)
showMessageDialog = false
messageToSend = ""
}
},
) { Text("Send") }
},
dismissButton = {
... ... @@ -324,7 +343,7 @@ class CallActivity : AppCompatActivity() {
onClick = {
showMessageDialog = false
messageToSend = ""
}
},
) { Text("Cancel") }
},
backgroundColor = Color.Black,
... ... @@ -334,7 +353,7 @@ class CallActivity : AppCompatActivity() {
onClick = { onExitClick() },
modifier = Modifier
.size(controlSize)
.padding(controlPadding)
.padding(controlPadding),
) {
Icon(
painterResource(id = R.drawable.ic_baseline_cancel_24),
... ... @@ -356,7 +375,7 @@ class CallActivity : AppCompatActivity() {
onClick = { showAudioDeviceDialog = true },
modifier = Modifier
.size(controlSize)
.padding(controlPadding)
.padding(controlPadding),
) {
val resource = R.drawable.volume_up_48px
Icon(
... ... @@ -370,14 +389,14 @@ class CallActivity : AppCompatActivity() {
onDismissRequest = { showAudioDeviceDialog = false },
selectDevice = { audioSwitchHandler?.selectDevice(it) },
currentDevice = audioSwitchHandler?.selectedAudioDevice,
availableDevices = audioSwitchHandler?.availableAudioDevices ?: emptyList()
availableDevices = audioSwitchHandler?.availableAudioDevices ?: emptyList(),
)
}
Surface(
onClick = { viewModel.toggleSubscriptionPermissions() },
modifier = Modifier
.size(controlSize)
.padding(controlPadding)
.padding(controlPadding),
) {
val resource =
if (permissionAllowed) R.drawable.account_cancel_outline else R.drawable.account_cancel
... ... @@ -393,7 +412,7 @@ class CallActivity : AppCompatActivity() {
onClick = { showDebugDialog = true },
modifier = Modifier
.size(controlSize)
.padding(controlPadding)
.padding(controlPadding),
) {
val resource = R.drawable.dots_horizontal_circle_outline
Icon(
... ... @@ -427,7 +446,7 @@ class CallActivity : AppCompatActivity() {
scope.launch {
scaffoldState.snackbarHostState.showSnackbar(error?.toString() ?: "")
}
}
},
)
},
content = { innerPadding ->
... ... @@ -436,9 +455,9 @@ class CallActivity : AppCompatActivity() {
modifier = Modifier
.padding(innerPadding)
.fillMaxSize()
.wrapContentSize()
.wrapContentSize(),
)
}
},
)
}
}
... ... @@ -454,6 +473,6 @@ class CallActivity : AppCompatActivity() {
val url: String,
val token: String,
val e2eeKey: String,
val e2eeOn: Boolean
val e2eeOn: Boolean,
) : Parcelable
}
... ...
/*
* Copyright 2023 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.composesample
import android.content.Intent
... ... @@ -38,6 +54,7 @@ import androidx.compose.ui.unit.dp
import com.google.accompanist.pager.ExperimentalPagerApi
import io.livekit.android.composesample.ui.theme.AppTheme
import io.livekit.android.sample.MainViewModel
import io.livekit.android.sample.common.R
import io.livekit.android.sample.util.requestNeededPermissions
@ExperimentalPagerApi
... ...
/*
* Copyright 2023 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.composesample
import androidx.compose.foundation.background
... ... @@ -20,6 +36,7 @@ import io.livekit.android.composesample.ui.theme.NoVideoBackground
import io.livekit.android.room.Room
import io.livekit.android.room.participant.ConnectionQuality
import io.livekit.android.room.participant.Participant
import io.livekit.android.sample.common.R
import io.livekit.android.util.flow
/**
... ...
/*
* Copyright 2023 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.composesample
import android.app.Application
... ... @@ -9,5 +25,6 @@ class SampleApplication : Application() {
override fun onCreate() {
super.onCreate()
LiveKit.loggingLevel = LoggingLevel.VERBOSE
LiveKit.enableWebRTCLogging = true
}
}
... ...
/*
* Copyright 2023 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.composesample
import androidx.compose.foundation.layout.Box
import androidx.compose.material.Icon
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import io.livekit.android.compose.VideoRenderer
import io.livekit.android.composesample.ui.VideoRenderer
import io.livekit.android.room.Room
import io.livekit.android.room.participant.Participant
import io.livekit.android.room.track.CameraPosition
import io.livekit.android.room.track.LocalVideoTrack
import io.livekit.android.room.track.Track
import io.livekit.android.room.track.VideoTrack
import io.livekit.android.sample.common.R
import io.livekit.android.util.flow
/**
... ... @@ -60,7 +83,7 @@ fun VideoItemTrackSelector(
room = room,
videoTrack = videoTrack,
mirror = room.localParticipant == participant && cameraFacingFront,
modifier = modifier
modifier = modifier,
)
} else {
Box(modifier = modifier) {
... ... @@ -68,7 +91,7 @@ fun VideoItemTrackSelector(
painter = painterResource(id = R.drawable.outline_videocam_off_24),
contentDescription = null,
tint = Color.White,
modifier = Modifier.align(Alignment.Center)
modifier = Modifier.align(Alignment.Center),
)
}
}
... ...
/*
* Copyright 2023 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.composesample.ui
import androidx.compose.ui.layout.LayoutCoordinates
import io.livekit.android.room.track.Track
import io.livekit.android.room.track.video.VideoSinkVisibility
/**
*
*/
class ComposeVisibility : VideoSinkVisibility() {
private var coordinates: LayoutCoordinates? = null
private var lastVisible = isVisible()
private var lastSize = size()
override fun isVisible(): Boolean {
return (coordinates?.isAttached == true &&
coordinates?.size?.width != 0 &&
coordinates?.size?.height != 0)
}
override fun size(): Track.Dimensions {
val width = coordinates?.size?.width ?: 0
val height = coordinates?.size?.height ?: 0
return Track.Dimensions(width, height)
}
// Note, LayoutCoordinates are mutable and may be reused.
fun onGloballyPositioned(layoutCoordinates: LayoutCoordinates) {
coordinates = layoutCoordinates
val visible = isVisible()
val size = size()
if (lastVisible != visible || lastSize != size) {
notifyChanged()
}
lastVisible = visible
lastSize = size
}
fun onDispose() {
if (coordinates == null) {
return
}
coordinates = null
notifyChanged()
}
}
... ...
... ... @@ -14,17 +14,32 @@
* limitations under the License.
*/
package io.livekit.android.compose
package io.livekit.android.composesample.ui
import androidx.compose.runtime.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.currentCompositeKeyHash
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.viewinterop.AndroidView
import io.livekit.android.renderer.TextureViewRenderer
import io.livekit.android.room.Room
import io.livekit.android.room.track.RemoteVideoTrack
import io.livekit.android.room.track.VideoTrack
import io.livekit.android.room.track.video.ComposeVisibility
import org.webrtc.RendererCommon
enum class ScaleType {
FitInside,
Fill,
}
/**
* Widget for displaying a VideoTrack. Handles the Compose <-> AndroidView interop needed to use
... ... @@ -33,10 +48,21 @@ import io.livekit.android.room.track.video.ComposeVisibility
@Composable
fun VideoRenderer(
room: Room,
videoTrack: VideoTrack,
videoTrack: VideoTrack?,
modifier: Modifier = Modifier,
mirror: Boolean = false,
scaleType: ScaleType = ScaleType.Fill,
) {
// Show a black box for preview.
if (LocalView.current.isInEditMode) {
Box(
modifier = Modifier
.background(Color.Black)
.then(modifier)
)
return
}
val videoSinkVisibility = remember(room, videoTrack) { ComposeVisibility() }
var boundVideoTrack by remember { mutableStateOf<VideoTrack?>(null) }
var view: TextureViewRenderer? by remember { mutableStateOf(null) }
... ... @@ -46,7 +72,7 @@ fun VideoRenderer(
boundVideoTrack = null
}
fun setupVideoIfNeeded(videoTrack: VideoTrack, view: TextureViewRenderer) {
fun setupVideoIfNeeded(videoTrack: VideoTrack?, view: TextureViewRenderer) {
if (boundVideoTrack == videoTrack) {
return
}
... ... @@ -54,10 +80,12 @@ fun VideoRenderer(
cleanupVideoTrack()
boundVideoTrack = videoTrack
if (videoTrack is RemoteVideoTrack) {
videoTrack.addRenderer(view, videoSinkVisibility)
} else {
videoTrack.addRenderer(view)
if (videoTrack != null) {
if (videoTrack is RemoteVideoTrack) {
videoTrack.addRenderer(view, videoSinkVisibility)
} else {
videoTrack.addRenderer(view)
}
}
}
... ... @@ -85,6 +113,15 @@ fun VideoRenderer(
room.initVideoRenderer(this)
setupVideoIfNeeded(videoTrack, this)
when (scaleType) {
ScaleType.FitInside -> {
this.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
}
ScaleType.Fill -> {
this.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL)
}
}
view = this
}
},
... ...
... ... @@ -4,12 +4,14 @@ plugins {
}
android {
compileSdk 32
namespace 'io.livekit.android.sample.record'
compileSdk androidSdk.compileVersion
defaultConfig {
applicationId "io.livekit.android.sample.record"
minSdk 21
targetSdk 32
minSdk androidSdk.minVersion
targetSdk androidSdk.targetVersion
versionCode 1
versionName "1.0"
... ... @@ -26,11 +28,11 @@ android {
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility java_version
targetCompatibility java_version
}
kotlinOptions {
jvmTarget = '1.8'
jvmTarget = java_version
}
buildFeatures {
compose true
... ...
... ... @@ -5,8 +5,9 @@ plugins {
}
android {
namespace "io.livekit.android.sample"
compileSdkVersion androidSdk.compileVersion
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "io.livekit.android"
minSdkVersion androidSdk.minVersion
... ...
/*
* Copyright 2023 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.sample
import android.app.Activity
... ... @@ -13,6 +29,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.xwray.groupie.GroupieAdapter
import io.livekit.android.sample.common.R
import io.livekit.android.sample.databinding.CallActivityBinding
import io.livekit.android.sample.dialog.showDebugMenuDialog
import io.livekit.android.sample.dialog.showSelectAudioDeviceDialog
... ... @@ -29,7 +46,7 @@ class CallActivity : AppCompatActivity() {
lateinit var binding: CallActivityBinding
private val screenCaptureIntentLauncher =
registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
ActivityResultContracts.StartActivityForResult(),
) { result ->
val resultCode = result.resultCode
val data = result.data
... ... @@ -83,7 +100,7 @@ class CallActivity : AppCompatActivity() {
R.drawable.outline_videocam_24
} else {
R.drawable.outline_videocam_off_24
}
},
)
binding.flipCamera.isEnabled = enabled
}
... ... @@ -94,7 +111,7 @@ class CallActivity : AppCompatActivity() {
R.drawable.outline_mic_24
} else {
R.drawable.outline_mic_off_24
}
},
)
}
... ... @@ -112,7 +129,7 @@ class CallActivity : AppCompatActivity() {
R.drawable.baseline_cast_connected_24
} else {
R.drawable.baseline_cast_24
}
},
)
}
... ...
... ... @@ -4,7 +4,7 @@ pluginManagement {
}
}
include ':sample-app', ':sample-app-compose', ':livekit-android-sdk'
rootProject.name='livekit-android'
rootProject.name = 'livekit-android'
include ':sample-app-common'
include ':livekit-lint'
include ':video-encode-decode-test'
... ...
... ... @@ -21,10 +21,11 @@ final apiKey = getApiKey()
final apiSecret = getApiSecret()
android {
namespace "io.livekit.android.videoencodedecode"
compileSdkVersion androidSdk.compileVersion
defaultConfig {
applicationId "io.livekit.android.videoencodedecodetest"
applicationId "io.livekit.android.videoencodedecode"
minSdkVersion androidSdk.minVersion
targetSdkVersion androidSdk.targetVersion
versionCode 1
... ... @@ -60,7 +61,8 @@ android {
freeCompilerArgs += '-opt-in=kotlin.RequiresOptIn'
}
buildFeatures {
compose true
buildConfig = true
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion compose_compiler_version
... ...
/*
* Copyright 2023 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.videoencodedecode
import android.Manifest
... ... @@ -21,6 +37,7 @@ import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import com.google.accompanist.pager.ExperimentalPagerApi
import io.livekit.android.composesample.ui.theme.AppTheme
import io.livekit.android.sample.common.R
@ExperimentalPagerApi
class MainActivity : ComponentActivity() {
... ...
/*
* Copyright 2023 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.videoencodedecode
import androidx.compose.foundation.background
... ... @@ -20,6 +36,7 @@ import io.livekit.android.composesample.ui.theme.NoVideoBackground
import io.livekit.android.room.Room
import io.livekit.android.room.participant.ConnectionQuality
import io.livekit.android.room.participant.Participant
import io.livekit.android.sample.common.R
import io.livekit.android.util.flow
/**
... ...
/*
* Copyright 2023 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.videoencodedecode
import androidx.compose.foundation.layout.Box
... ... @@ -21,8 +37,8 @@ import io.livekit.android.room.participant.Participant
import io.livekit.android.room.track.RemoteVideoTrack
import io.livekit.android.room.track.Track
import io.livekit.android.room.track.VideoTrack
import io.livekit.android.room.track.video.ComposeVisibility
import io.livekit.android.util.flow
import io.livekit.android.videoencodedecode.ui.ComposeVisibility
import kotlinx.coroutines.flow.*
/**
... ... @@ -137,7 +153,7 @@ fun VideoItemTrackSelector(
} else {
Box(modifier = modifier) {
Icon(
painter = painterResource(id = R.drawable.outline_videocam_off_24),
painter = painterResource(id = io.livekit.android.sample.common.R.drawable.outline_videocam_off_24),
contentDescription = null,
tint = Color.White,
modifier = Modifier.align(Alignment.Center)
... ...
/*
* Copyright 2023 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.videoencodedecode.ui
import androidx.compose.ui.layout.LayoutCoordinates
import io.livekit.android.room.track.Track
import io.livekit.android.room.track.video.VideoSinkVisibility
/**
*
*/
class ComposeVisibility : VideoSinkVisibility() {
private var coordinates: LayoutCoordinates? = null
private var lastVisible = isVisible()
private var lastSize = size()
override fun isVisible(): Boolean {
return (coordinates?.isAttached == true &&
coordinates?.size?.width != 0 &&
coordinates?.size?.height != 0)
}
override fun size(): Track.Dimensions {
val width = coordinates?.size?.width ?: 0
val height = coordinates?.size?.height ?: 0
return Track.Dimensions(width, height)
}
// Note, LayoutCoordinates are mutable and may be reused.
fun onGloballyPositioned(layoutCoordinates: LayoutCoordinates) {
coordinates = layoutCoordinates
val visible = isVisible()
val size = size()
if (lastVisible != visible || lastSize != size) {
notifyChanged()
}
lastVisible = visible
lastSize = size
}
fun onDispose() {
if (coordinates == null) {
return
}
coordinates = null
notifyChanged()
}
}
... ...
/*
* Copyright 2023 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.videoencodedecode.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.currentCompositeKeyHash
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.viewinterop.AndroidView
import io.livekit.android.renderer.TextureViewRenderer
import io.livekit.android.room.Room
import io.livekit.android.room.track.RemoteVideoTrack
import io.livekit.android.room.track.VideoTrack
import org.webrtc.RendererCommon
enum class ScaleType {
FitInside,
Fill,
}
/**
* Widget for displaying a VideoTrack. Handles the Compose <-> AndroidView interop needed to use
* [TextureViewRenderer].
*/
@Composable
fun VideoRenderer(
room: Room,
videoTrack: VideoTrack?,
modifier: Modifier = Modifier,
mirror: Boolean = false,
scaleType: ScaleType = ScaleType.Fill,
) {
// Show a black box for preview.
if (LocalView.current.isInEditMode) {
Box(
modifier = Modifier
.background(Color.Black)
.then(modifier)
)
return
}
val videoSinkVisibility = remember(room, videoTrack) { ComposeVisibility() }
var boundVideoTrack by remember { mutableStateOf<VideoTrack?>(null) }
var view: TextureViewRenderer? by remember { mutableStateOf(null) }
fun cleanupVideoTrack() {
view?.let { boundVideoTrack?.removeRenderer(it) }
boundVideoTrack = null
}
fun setupVideoIfNeeded(videoTrack: VideoTrack?, view: TextureViewRenderer) {
if (boundVideoTrack == videoTrack) {
return
}
cleanupVideoTrack()
boundVideoTrack = videoTrack
if (videoTrack != null) {
if (videoTrack is RemoteVideoTrack) {
videoTrack.addRenderer(view, videoSinkVisibility)
} else {
videoTrack.addRenderer(view)
}
}
}
DisposableEffect(view, mirror) {
view?.setMirror(mirror)
onDispose { }
}
DisposableEffect(room, videoTrack) {
onDispose {
videoSinkVisibility.onDispose()
cleanupVideoTrack()
}
}
DisposableEffect(currentCompositeKeyHash.toString()) {
onDispose {
view?.release()
}
}
AndroidView(
factory = { context ->
TextureViewRenderer(context).apply {
room.initVideoRenderer(this)
setupVideoIfNeeded(videoTrack, this)
when (scaleType) {
ScaleType.FitInside -> {
this.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
}
ScaleType.Fill -> {
this.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL)
}
}
view = this
}
},
update = { v -> setupVideoIfNeeded(videoTrack, v) },
modifier = modifier
.onGloballyPositioned { videoSinkVisibility.onGloballyPositioned(it) },
)
}
... ...