davidliu
Committed by GitHub

Compose sample (#8)

* intermediate commit

* compose sample

* inspection profiles
正在显示 46 个修改的文件 包含 1295 行增加9 行删除
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="previewFile" value="true" />
</inspection_tool>
</profile>
</component>
\ No newline at end of file
... ...
... ... @@ -2,5 +2,6 @@
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/protocol" vcs="Git" />
</component>
</project>
\ No newline at end of file
... ...
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.5.21'
ext.java_version = JavaVersion.VERSION_1_8
ext.dokka_version = '1.5.0'
ext {
compose_version = '1.0.3'
kotlin_version = '1.5.30'
java_version = JavaVersion.VERSION_1_8
dokka_version = '1.5.0'
}
repositories {
google()
mavenCentral()
... ... @@ -58,6 +61,8 @@ ext {
protoSrc: "$projectDir/protocol",
]
deps = [
kotlinx_coroutines: "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2",
timber : "com.github.ajalt:timberkt:1.5.1",
]
annotations = [
]
... ...
... ... @@ -97,7 +97,7 @@ dependencies {
protobuf files(generated.protoSrc)
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3'
implementation deps.kotlinx_coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0'
api 'org.webrtc:google-webrtc:1.0.32006'
api "com.squareup.okhttp3:okhttp:4.9.0"
... ... @@ -107,7 +107,7 @@ dependencies {
implementation 'com.google.dagger:dagger:2.38'
kapt 'com.google.dagger:dagger-compiler:2.38'
implementation 'com.github.ajalt:timberkt:1.5.1'
implementation deps.timber
implementation 'com.vdurmont:semver4j:3.1.0'
testImplementation 'junit:junit:4.13.2'
... ...
/build
\ No newline at end of file
... ...
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-parcelize'
}
android {
compileSdkVersion 30
defaultConfig {
applicationId "io.livekit.android.composesample"
minSdkVersion 21
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility java_version
targetCompatibility java_version
}
kotlinOptions {
jvmTarget = java_version
freeCompilerArgs += '-Xopt-in=kotlin.RequiresOptIn'
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion compose_version
kotlinCompilerVersion kotlin_version
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation deps.kotlinx_coroutines
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.4.0'
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
implementation "androidx.compose.runtime:runtime-livedata:$compose_version"
implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-rc01"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:${versions.androidx_lifecycle}"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${versions.androidx_lifecycle}"
implementation "androidx.lifecycle:lifecycle-common-java8:${versions.androidx_lifecycle}"
implementation 'androidx.activity:activity-compose:1.3.1'
implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'com.google.accompanist:accompanist-pager:0.19.0'
implementation 'com.google.accompanist:accompanist-pager-indicators:0.19.0'
implementation deps.timber
implementation project(":livekit-android-sdk")
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
}
\ No newline at end of file
... ...
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
\ No newline at end of file
... ...
package io.livekit.android.composesample
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("io.livekit.android.composesample", appContext.packageName)
}
}
\ No newline at end of file
... ...
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.livekit.android.composesample">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
<application
android:allowBackup="true"
android:name=".SampleApplication"
android:fullBackupContent="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true"
android:theme="@style/Theme.Livekitandroid">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Livekitandroid.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".CallActivity"
android:theme="@style/Theme.Livekitandroid.NoActionBar" />
</application>
</manifest>
\ No newline at end of file
... ...
package io.livekit.android.composesample
import android.media.AudioManager
import android.os.Bundle
import android.os.Parcelable
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.TabRowDefaults.tabIndicatorOffset
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import com.github.ajalt.timberkt.Timber
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.rememberPagerState
import io.livekit.android.composesample.ui.theme.AppTheme
import io.livekit.android.renderer.TextureViewRenderer
import io.livekit.android.room.Room
import io.livekit.android.room.participant.RemoteParticipant
import io.livekit.android.room.track.LocalVideoTrack
import kotlinx.parcelize.Parcelize
@OptIn(ExperimentalPagerApi::class)
class CallActivity : AppCompatActivity() {
private val viewModel: CallViewModel by viewModelByFactory {
val args = intent.getParcelableExtra<BundleArgs>(KEY_ARGS)
?: throw NullPointerException("args is null!")
CallViewModel(args.url, args.token, application)
}
private val focusChangeListener = AudioManager.OnAudioFocusChangeListener {}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
with(audioManager) {
isSpeakerphoneOn = true
isMicrophoneMute = false
mode = AudioManager.MODE_IN_COMMUNICATION
}
val result = audioManager.requestAudioFocus(
focusChangeListener,
AudioManager.STREAM_VOICE_CALL,
AudioManager.AUDIOFOCUS_GAIN,
)
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
Timber.v { "Audio focus request granted for VOICE_CALL streams" }
} else {
Timber.v { "Audio focus request failed" }
}
setContent {
AppTheme(darkTheme = true) {
val room by viewModel.room.observeAsState()
val participants by viewModel.remoteParticipants.observeAsState(emptyList())
val micEnabled by viewModel.micEnabled.observeAsState(true)
val videoEnabled by viewModel.videoEnabled.observeAsState(true)
val flipButtonEnabled by viewModel.flipButtonVideoEnabled.observeAsState(true)
Content(
room,
participants,
micEnabled,
videoEnabled,
flipButtonEnabled
)
}
}
}
@Preview(showBackground = true, showSystemUi = true)
@Composable
fun Content(
room: Room? = null,
participants: List<RemoteParticipant> = emptyList(),
micEnabled: Boolean = true,
videoEnabled: Boolean = true,
flipButtonEnabled: Boolean = true,
) {
ConstraintLayout(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.background)
) {
val (tabRow, pager, buttonBar, cameraView) = createRefs()
if (participants.isNotEmpty()) {
val pagerState = rememberPagerState()
ScrollableTabRow(
// Our selected tab is our current page
selectedTabIndex = pagerState.currentPage,
// Override the indicator, using the provided pagerTabIndicatorOffset modifier
indicator = { tabPositions ->
TabRowDefaults.Indicator(
modifier = Modifier
.height(1.dp)
.tabIndicatorOffset(tabPositions[pagerState.currentPage]),
height = 1.dp,
color = Color.Gray
)
},
modifier = Modifier
.background(Color.DarkGray)
.constrainAs(tabRow) {
top.linkTo(parent.top)
width = Dimension.fillToConstraints
}
) {
// Add tabs for all of our pages
participants.forEachIndexed { index, participant ->
Tab(
text = { Text(participant.identity ?: "Unnamed $index") },
selected = pagerState.currentPage == index,
onClick = { /* TODO*/ },
)
}
}
HorizontalPager(
count = participants.size,
state = pagerState,
modifier = Modifier
.constrainAs(pager) {
top.linkTo(tabRow.bottom)
bottom.linkTo(buttonBar.top)
start.linkTo(parent.start)
end.linkTo(parent.end)
width = Dimension.fillToConstraints
height = Dimension.fillToConstraints
}
) { index ->
if (room != null) {
ParticipantItem(room = room, participant = participants[index])
}
}
}
if (room != null) {
var videoNeedsSetup by remember { mutableStateOf(true) }
AndroidView(
factory = { context ->
TextureViewRenderer(context).apply {
room.initVideoRenderer(this)
}
},
modifier = Modifier
.width(200.dp)
.height(200.dp)
.padding(bottom = 10.dp, end = 10.dp)
.background(Color.Black)
.constrainAs(cameraView) {
bottom.linkTo(buttonBar.top)
end.linkTo(parent.end)
},
update = { view ->
val videoTrack = room.localParticipant.videoTracks.values
.firstOrNull()
?.track as? LocalVideoTrack
if (videoNeedsSetup) {
videoTrack?.addRenderer(view)
videoNeedsSetup = false
}
}
)
}
Row(
modifier = Modifier
.padding(top = 10.dp, bottom = 20.dp)
.fillMaxWidth()
.constrainAs(buttonBar) {
bottom.linkTo(parent.bottom)
width = Dimension.fillToConstraints
},
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.Bottom,
) {
FloatingActionButton(
onClick = { viewModel.setMicEnabled(!micEnabled) },
backgroundColor = Color.DarkGray,
) {
val resource =
if (micEnabled) R.drawable.outline_mic_24 else R.drawable.outline_mic_off_24
Icon(
painterResource(id = resource),
contentDescription = "Mic",
tint = Color.White,
)
}
FloatingActionButton(
onClick = { viewModel.setVideoEnabled(!videoEnabled) },
backgroundColor = Color.DarkGray,
) {
val resource =
if (videoEnabled) R.drawable.outline_videocam_24 else R.drawable.outline_videocam_off_24
Icon(
painterResource(id = resource),
contentDescription = "Video",
tint = Color.White,
)
}
FloatingActionButton(
onClick = { viewModel.flipVideo() },
backgroundColor = Color.DarkGray,
) {
Icon(
painterResource(id = R.drawable.outline_flip_camera_android_24),
contentDescription = "Flip Camera",
tint = Color.White,
)
}
}
}
}
override fun onDestroy() {
super.onDestroy()
val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
with(audioManager) {
isSpeakerphoneOn = false
isMicrophoneMute = true
abandonAudioFocus(focusChangeListener)
mode = AudioManager.MODE_NORMAL
}
}
companion object {
const val KEY_ARGS = "args"
}
@Parcelize
data class BundleArgs(val url: String, val token: String) : Parcelable
}
\ No newline at end of file
... ...
package io.livekit.android.composesample
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.github.ajalt.timberkt.Timber
import io.livekit.android.ConnectOptions
import io.livekit.android.LiveKit
import io.livekit.android.room.Room
import io.livekit.android.room.RoomListener
import io.livekit.android.room.participant.Participant
import io.livekit.android.room.participant.RemoteParticipant
import io.livekit.android.room.track.LocalAudioTrack
import io.livekit.android.room.track.LocalVideoTrack
import kotlinx.coroutines.launch
class CallViewModel(
val url: String,
val token: String,
application: Application
) : AndroidViewModel(application), RoomListener {
private val mutableRoom = MutableLiveData<Room>()
val room: LiveData<Room> = mutableRoom
private val mutableRemoteParticipants = MutableLiveData<List<RemoteParticipant>>()
val remoteParticipants: LiveData<List<RemoteParticipant>> = mutableRemoteParticipants
private var localAudioTrack: LocalAudioTrack? = null
private var localVideoTrack: LocalVideoTrack? = null
private val mutableMicEnabled = MutableLiveData(true)
val micEnabled = mutableMicEnabled.hide()
private val mutableVideoEnabled = MutableLiveData(true)
val videoEnabled = mutableVideoEnabled.hide()
private val mutableFlipVideoButtonEnabled = MutableLiveData(true)
val flipButtonVideoEnabled = mutableFlipVideoButtonEnabled.hide()
init {
viewModelScope.launch {
val room = LiveKit.connect(
application,
url,
token,
ConnectOptions(),
this@CallViewModel
)
val localParticipant = room.localParticipant
val audioTrack = localParticipant.createAudioTrack()
localParticipant.publishAudioTrack(audioTrack)
this@CallViewModel.localAudioTrack = audioTrack
mutableMicEnabled.postValue(audioTrack.enabled)
val videoTrack = localParticipant.createVideoTrack()
localParticipant.publishVideoTrack(videoTrack)
videoTrack.startCapture()
this@CallViewModel.localVideoTrack = videoTrack
mutableVideoEnabled.postValue(videoTrack.enabled)
updateParticipants(room)
mutableRoom.value = room
}
}
private fun updateParticipants(room: Room) {
mutableRemoteParticipants.postValue(
room.remoteParticipants
.keys
.sortedBy { it }
.mapNotNull { room.remoteParticipants[it] }
)
}
override fun onCleared() {
super.onCleared()
mutableRoom.value?.disconnect()
}
override fun onDisconnect(room: Room, error: Exception?) {
}
override fun onParticipantConnected(
room: Room,
participant: RemoteParticipant
) {
updateParticipants(room)
}
override fun onParticipantDisconnected(
room: Room,
participant: RemoteParticipant
) {
updateParticipants(room)
}
override fun onFailedToConnect(room: Room, error: Exception) {
}
override fun onActiveSpeakersChanged(speakers: List<Participant>, room: Room) {
Timber.i { "active speakers changed ${speakers.count()}" }
}
override fun onMetadataChanged(participant: Participant, prevMetadata: String?, room: Room) {
Timber.i { "Participant metadata changed: ${participant.identity}" }
}
fun setMicEnabled(enabled: Boolean) {
localAudioTrack?.enabled = enabled
mutableMicEnabled.postValue(enabled)
}
fun setVideoEnabled(enabled: Boolean) {
localVideoTrack?.enabled = enabled
mutableVideoEnabled.postValue(enabled)
}
fun flipVideo() {
// TODO
}
}
private fun <T> LiveData<T>.hide(): LiveData<T> = this
... ...
package io.livekit.android.composesample
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.text.SpannableStringBuilder
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import com.google.accompanist.pager.ExperimentalPagerApi
import io.livekit.android.composesample.ui.theme.AppTheme
@ExperimentalPagerApi
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestPermissions()
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
val defaultUrl = preferences.getString(PREFERENCES_KEY_URL, URL) as String
val defaultToken = preferences.getString(PREFERENCES_KEY_TOKEN, TOKEN) as String
setContent {
MainContent(
defaultUrl = defaultUrl,
defaultToken = defaultToken,
onConnect = { url, token ->
val intent = Intent(this@MainActivity, CallActivity::class.java).apply {
putExtra(
CallActivity.KEY_ARGS,
CallActivity.BundleArgs(
url,
token
)
)
}
startActivity(intent)
},
onSave = { url, token ->
preferences.edit {
putString(PREFERENCES_KEY_URL, url)
putString(PREFERENCES_KEY_TOKEN, token)
}
Toast.makeText(
this@MainActivity,
"Values saved.",
Toast.LENGTH_SHORT
).show()
},
onReset = {
preferences.edit {
clear()
}
Toast.makeText(
this@MainActivity,
"Values reset.",
Toast.LENGTH_SHORT
).show()
}
)
}
}
@Preview(
showBackground = true,
showSystemUi = true,
)
@Composable
fun MainContent(
defaultUrl: String = URL,
defaultToken: String = TOKEN,
onConnect: (url: String, token: String) -> Unit = { _, _ -> },
onSave: (url: String, token: String) -> Unit = { _, _ -> },
onReset: () -> Unit = {},
) {
AppTheme {
var url by remember { mutableStateOf(defaultUrl) }
var token by remember { mutableStateOf(defaultToken) }
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(10.dp)
) {
OutlinedTextField(
value = url,
onValueChange = { url = it },
label = { Text("URL") },
modifier = Modifier.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(20.dp))
OutlinedTextField(
value = token,
onValueChange = { token = it },
label = { Text("Token") },
modifier = Modifier.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(20.dp))
Button(onClick = { onConnect(url, token) }) {
Text("Connect")
}
Spacer(modifier = Modifier.height(20.dp))
Button(onClick = { onSave(url, token) }) {
Text("Save Values")
}
Spacer(modifier = Modifier.height(20.dp))
Button(onClick = {
url = URL
token = TOKEN
onReset()
}) {
Text("Reset Values")
}
}
}
}
}
private fun requestPermissions() {
val requestPermissionLauncher =
registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { grants ->
for (grant in grants.entries) {
if (!grant.value) {
Toast.makeText(
this,
"Missing permission: ${grant.key}",
Toast.LENGTH_SHORT
)
.show()
}
}
}
val neededPermissions = listOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
.filter {
ContextCompat.checkSelfPermission(
this,
it
) == PackageManager.PERMISSION_DENIED
}
.toTypedArray()
if (neededPermissions.isNotEmpty()) {
requestPermissionLauncher.launch(neededPermissions)
}
}
companion object {
const val PREFERENCES_KEY_URL = "url"
const val PREFERENCES_KEY_TOKEN = "token"
const val URL = "wss://livekit.watercooler.fm"
const val TOKEN =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE5ODQyMzE0OTgsImlzcyI6IkFQSU1teGlMOHJxdUt6dFpFb1pKVjlGYiIsImp0aSI6ImZvcnRoIiwibmJmIjoxNjI0MjMxNDk4LCJ2aWRlbyI6eyJyb29tIjoibXlyb29tIiwicm9vbUpvaW4iOnRydWV9fQ.PVx_lXAIGxcD2VRslosrbkigc777GXbu-DQME8hjJKI"
}
}
... ...
package io.livekit.android.composesample
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import com.github.ajalt.timberkt.Timber
import io.livekit.android.renderer.TextureViewRenderer
import io.livekit.android.room.Room
import io.livekit.android.room.participant.ParticipantListener
import io.livekit.android.room.participant.RemoteParticipant
import io.livekit.android.room.track.RemoteTrackPublication
import io.livekit.android.room.track.Track
import io.livekit.android.room.track.VideoTrack
@Composable
fun ParticipantItem(
room: Room,
participant: RemoteParticipant,
) {
var videoBound by remember(room, participant) { mutableStateOf(false) }
fun getVideoTrack(): VideoTrack? {
return participant
.videoTracks.values
.firstOrNull()?.track as? VideoTrack
}
fun setupVideoIfNeeded(videoTrack: VideoTrack, view: TextureViewRenderer) {
if (videoBound) {
return
}
videoBound = true
Timber.v { "adding renderer to $videoTrack" }
videoTrack.addRenderer(view)
}
AndroidView(
factory = { context ->
TextureViewRenderer(context).apply {
room.initVideoRenderer(this)
}
},
modifier = Modifier.fillMaxSize(),
update = { view ->
participant.listener = object : ParticipantListener {
override fun onTrackSubscribed(
track: Track,
publication: RemoteTrackPublication,
participant: RemoteParticipant
) {
if (track is VideoTrack) {
setupVideoIfNeeded(track, view)
}
}
override fun onTrackUnpublished(
publication: RemoteTrackPublication,
participant: RemoteParticipant
) {
super.onTrackUnpublished(publication, participant)
Timber.e { "Track unpublished" }
}
}
val existingTrack = getVideoTrack()
if (existingTrack != null) {
setupVideoIfNeeded(existingTrack, view)
}
}
)
}
\ No newline at end of file
... ...
package io.livekit.android.composesample
import android.app.Application
import timber.log.Timber
class SampleApplication : Application() {
override fun onCreate() {
super.onCreate()
Timber.plant(Timber.DebugTree())
}
}
\ No newline at end of file
... ...
package io.livekit.android.composesample
import androidx.activity.viewModels
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
typealias CreateViewModel<VM> = () -> VM
inline fun <reified VM : ViewModel> FragmentActivity.viewModelByFactory(
noinline create: CreateViewModel<VM>
): Lazy<VM> {
return viewModels {
createViewModelFactoryFactory(create)
}
}
fun <VM> createViewModelFactoryFactory(
create: CreateViewModel<VM>
): ViewModelProvider.Factory {
return object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return create() as? T
?: throw IllegalArgumentException("Unknown viewmodel class!")
}
}
}
\ No newline at end of file
... ...
package io.livekit.android.composesample.ui.theme
import androidx.compose.ui.graphics.Color
val Purple200 = Color(0xFFBB86FC)
val Purple500 = Color(0xFF6200EE)
val Purple700 = Color(0xFF3700B3)
val Teal200 = Color(0xFF03DAC5)
\ No newline at end of file
... ...
package io.livekit.android.composesample.ui.theme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Shapes
import androidx.compose.ui.unit.dp
val Shapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(4.dp),
large = RoundedCornerShape(0.dp)
)
\ No newline at end of file
... ...
package io.livekit.android.composesample.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.MaterialTheme
import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
private val DarkColorPalette = darkColors(
primary = Purple200,
primaryVariant = Purple700,
secondary = Teal200,
background = Color.Black
)
private val LightColorPalette = lightColors(
primary = Purple500,
primaryVariant = Purple700,
secondary = Teal200
/* Other default colors to override
background = Color.White,
surface = Color.White,
onPrimary = Color.White,
onSecondary = Color.Black,
onBackground = Color.Black,
onSurface = Color.Black,
*/
)
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable() () -> Unit
) {
val colors = if (darkTheme) {
DarkColorPalette
} else {
LightColorPalette
}
MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
}
\ No newline at end of file
... ...
package io.livekit.android.composesample.ui.theme
import androidx.compose.material.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
body1 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
)
/* Other default text styles to override
button = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.W500,
fontSize = 14.sp
),
caption = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 12.sp
)
*/
)
\ No newline at end of file
... ...
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
\ No newline at end of file
... ...
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M9,12c0,1.66 1.34,3 3,3s3,-1.34 3,-3s-1.34,-3 -3,-3S9,10.34 9,12zM13,12c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1s0.45,-1 1,-1S13,11.45 13,12z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M8,10V8H5.09C6.47,5.61 9.05,4 12,4c3.72,0 6.85,2.56 7.74,6h2.06c-0.93,-4.56 -4.96,-8 -9.8,-8C8.73,2 5.82,3.58 4,6.01V4H2v6H8z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M16,14v2h2.91c-1.38,2.39 -3.96,4 -6.91,4c-3.72,0 -6.85,-2.56 -7.74,-6H2.2c0.93,4.56 4.96,8 9.8,8c3.27,0 6.18,-1.58 8,-4.01V20h2v-6H16z"/>
</vector>
... ...
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,14c1.66,0 3,-1.34 3,-3V5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6C9,12.66 10.34,14 12,14z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M17,11c0,2.76 -2.24,5 -5,5s-5,-2.24 -5,-5H5c0,3.53 2.61,6.43 6,6.92V21h2v-3.08c3.39,-0.49 6,-3.39 6,-6.92H17z"/>
</vector>
... ...
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M10.8,4.9c0,-0.66 0.54,-1.2 1.2,-1.2s1.2,0.54 1.2,1.2l-0.01,3.91L15,10.6V5c0,-1.66 -1.34,-3 -3,-3 -1.54,0 -2.79,1.16 -2.96,2.65l1.76,1.76V4.9zM19,11h-1.7c0,0.58 -0.1,1.13 -0.27,1.64l1.27,1.27c0.44,-0.88 0.7,-1.87 0.7,-2.91zM4.41,2.86L3,4.27l6,6V11c0,1.66 1.34,3 3,3 0.23,0 0.44,-0.03 0.65,-0.08l1.66,1.66c-0.71,0.33 -1.5,0.52 -2.31,0.52 -2.76,0 -5.3,-2.1 -5.3,-5.1H5c0,3.41 2.72,6.23 6,6.72V21h2v-3.28c0.91,-0.13 1.77,-0.45 2.55,-0.9l4.2,4.2 1.41,-1.41L4.41,2.86z"/>
</vector>
... ...
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M15,8v8H5V8h10m1,-2H4c-0.55,0 -1,0.45 -1,1v10c0,0.55 0.45,1 1,1h12c0.55,0 1,-0.45 1,-1v-3.5l4,4v-11l-4,4V7c0,-0.55 -0.45,-1 -1,-1z"/>
</vector>
... ...
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M9.56,8l-2,-2 -4.15,-4.14L2,3.27 4.73,6L4,6c-0.55,0 -1,0.45 -1,1v10c0,0.55 0.45,1 1,1h12c0.21,0 0.39,-0.08 0.55,-0.18L19.73,21l1.41,-1.41 -8.86,-8.86L9.56,8zM5,16L5,8h1.73l8,8L5,16zM15,8v2.61l6,6L21,6.5l-4,4L17,7c0,-0.55 -0.45,-1 -1,-1h-5.61l2,2L15,8z"/>
</vector>
... ...
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
... ...
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
\ No newline at end of file
... ...
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
\ No newline at end of file
... ...
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Livekitandroid" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>
\ No newline at end of file
... ...
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
\ No newline at end of file
... ...
<resources>
<string name="app_name">Sample Compose</string>
</resources>
\ No newline at end of file
... ...
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Livekitandroid" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
<style name="Theme.Livekitandroid.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="Theme.Livekitandroid.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
<style name="Theme.Livekitandroid.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
</resources>
\ No newline at end of file
... ...
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">example.com</domain>
</domain-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>
\ No newline at end of file
... ...
package io.livekit.android.composesample
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
\ No newline at end of file
... ...
... ... @@ -35,22 +35,22 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2'
implementation deps.kotlinx_coroutines
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.core:core-ktx:1.3.2'
implementation "androidx.activity:activity-ktx:1.2.2"
implementation 'androidx.fragment:fragment-ktx:1.3.2'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation 'androidx.preference:preference:1.1.1'
implementation "androidx.viewpager2:viewpager2:1.0.0"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:${versions.androidx_lifecycle}"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${versions.androidx_lifecycle}"
implementation "androidx.lifecycle:lifecycle-common-java8:${versions.androidx_lifecycle}"
implementation 'com.google.android.material:material:1.3.0'
implementation "com.xwray:groupie:${versions.groupie}"
implementation "com.xwray:groupie-viewbinding:${versions.groupie}"
implementation 'com.snakydesign.livedataextensions:lives:1.3.0'
implementation 'com.github.ajalt:timberkt:1.5.1'
implementation deps.timber
implementation project(":livekit-android-sdk")
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
... ...
... ... @@ -4,5 +4,5 @@ pluginManagement {
jcenter()
}
}
include ':sample-app', ':livekit-android-sdk'
include ':sample-app', ':sample-app-compose', ':livekit-android-sdk'
rootProject.name='livekit-android'
... ...