Committed by
GitHub
Update documentation and add a basic quickstart sample app. (#124)
* update readme and docs * clean up interface methods leaking into docs * AudioSwitchHandler documentation * Update package descriptions * Include link to API reference in README * update dependencies * Add in basic one participant sample app * clean up debug keys * update readme sample * Update README and add readmes for each project folder * Assemble all modules for compile test safety * Disconnect on destroy * update compile/target version * fix build errors * fix tests
正在显示
53 个修改的文件
包含
792 行增加
和
107 行删除
| @@ -36,7 +36,7 @@ jobs: | @@ -36,7 +36,7 @@ jobs: | ||
| 36 | run: chmod +x gradlew | 36 | run: chmod +x gradlew |
| 37 | 37 | ||
| 38 | - name: Build with Gradle | 38 | - name: Build with Gradle |
| 39 | - run: ./gradlew clean livekit-android-sdk:assembleRelease livekit-android-sdk:testRelease | 39 | + run: ./gradlew clean assembleRelease livekit-android-sdk:testRelease |
| 40 | 40 | ||
| 41 | - name: Import video test keys into gradle properties | 41 | - name: Import video test keys into gradle properties |
| 42 | if: github.event_name == 'push' | 42 | if: github.event_name == 'push' |
| 1 | # Android Kotlin SDK for LiveKit | 1 | # Android Kotlin SDK for LiveKit |
| 2 | 2 | ||
| 3 | -Official Android Client SDK for [LiveKit](https://github.com/livekit/livekit-server). Easily add video & audio capabilities to your Android apps. | 3 | +Official Android Client SDK for [LiveKit](https://github.com/livekit/livekit-server). Easily add |
| 4 | +video & audio capabilities to your Android apps. | ||
| 4 | 5 | ||
| 5 | ## Docs | 6 | ## Docs |
| 6 | 7 | ||
| 7 | -Docs and guides at [https://docs.livekit.io](https://docs.livekit.io) | 8 | +Docs and guides at [https://docs.livekit.io](https://docs.livekit.io). |
| 9 | + | ||
| 10 | +API reference can be found | ||
| 11 | +at [https://docs.livekit.io/client-sdk-android/index.html](https://docs.livekit.io/client-sdk-android/index.html) | ||
| 12 | +. | ||
| 8 | 13 | ||
| 9 | ## Installation | 14 | ## Installation |
| 10 | 15 | ||
| @@ -35,13 +40,6 @@ subprojects { | @@ -35,13 +40,6 @@ subprojects { | ||
| 35 | } | 40 | } |
| 36 | ``` | 41 | ``` |
| 37 | 42 | ||
| 38 | -## Sample App | ||
| 39 | - | ||
| 40 | -There are two sample apps with similar functionality: | ||
| 41 | - | ||
| 42 | -* [Compose app](https://github.com/livekit/client-sdk-android/tree/master/sample-app-compose/src/main/java/io/livekit/android/composesample) | ||
| 43 | -* [Standard app](https://github.com/livekit/client-sdk-android/tree/master/sample-app) | ||
| 44 | - | ||
| 45 | ## Usage | 43 | ## Usage |
| 46 | 44 | ||
| 47 | ### Permissions | 45 | ### Permissions |
| @@ -86,36 +84,47 @@ LiveKit uses `SurfaceViewRenderer` to render video tracks. A `TextureView` imple | @@ -86,36 +84,47 @@ LiveKit uses `SurfaceViewRenderer` to render video tracks. A `TextureView` imple | ||
| 86 | provided through `TextureViewRenderer`. Subscribed audio tracks are automatically played. | 84 | provided through `TextureViewRenderer`. Subscribed audio tracks are automatically played. |
| 87 | 85 | ||
| 88 | ```kt | 86 | ```kt |
| 89 | -class MainActivity : AppCompatActivity(), RoomListener { | 87 | +class MainActivity : AppCompatActivity() { |
| 88 | + | ||
| 89 | + lateinit var room: Room | ||
| 90 | + | ||
| 90 | override fun onCreate(savedInstanceState: Bundle?) { | 91 | override fun onCreate(savedInstanceState: Bundle?) { |
| 91 | super.onCreate(savedInstanceState) | 92 | super.onCreate(savedInstanceState) |
| 92 | - ... | ||
| 93 | - val url = "wss://your_host"; | 93 | + |
| 94 | + setContentView(R.layout.activity_main) | ||
| 95 | + | ||
| 96 | + // Create Room object. | ||
| 97 | + room = LiveKit.create(applicationContext) | ||
| 98 | + | ||
| 99 | + // Setup the video renderer | ||
| 100 | + room.initVideoRenderer(findViewById<SurfaceViewRenderer>(R.id.renderer)) | ||
| 101 | + | ||
| 102 | + connectToRoom() | ||
| 103 | + } | ||
| 104 | + | ||
| 105 | + private fun connectToRoom() { | ||
| 106 | + | ||
| 107 | + val url = "wss://your_host" | ||
| 94 | val token = "your_token" | 108 | val token = "your_token" |
| 95 | 109 | ||
| 96 | lifecycleScope.launch { | 110 | lifecycleScope.launch { |
| 97 | - // Create Room object. | ||
| 98 | - val room = LiveKit.create( | ||
| 99 | - applicationContext, | ||
| 100 | - RoomOptions(), | ||
| 101 | - ) | ||
| 102 | - | 111 | + |
| 103 | // Setup event handling. | 112 | // Setup event handling. |
| 104 | launch { | 113 | launch { |
| 105 | room.events.collect { event -> | 114 | room.events.collect { event -> |
| 106 | - when(event){ | 115 | + when (event) { |
| 107 | is RoomEvent.TrackSubscribed -> onTrackSubscribed(event) | 116 | is RoomEvent.TrackSubscribed -> onTrackSubscribed(event) |
| 117 | + else -> {} | ||
| 108 | } | 118 | } |
| 109 | } | 119 | } |
| 110 | } | 120 | } |
| 111 | - | 121 | + |
| 112 | // Connect to server. | 122 | // Connect to server. |
| 113 | room.connect( | 123 | room.connect( |
| 114 | url, | 124 | url, |
| 115 | token, | 125 | token, |
| 116 | - ConnectOptions() | ||
| 117 | ) | 126 | ) |
| 118 | - | 127 | + |
| 119 | // Turn on audio/video recording. | 128 | // Turn on audio/video recording. |
| 120 | val localParticipant = room.localParticipant | 129 | val localParticipant = room.localParticipant |
| 121 | localParticipant.setMicrophoneEnabled(true) | 130 | localParticipant.setMicrophoneEnabled(true) |
| @@ -124,22 +133,27 @@ class MainActivity : AppCompatActivity(), RoomListener { | @@ -124,22 +133,27 @@ class MainActivity : AppCompatActivity(), RoomListener { | ||
| 124 | } | 133 | } |
| 125 | 134 | ||
| 126 | private fun onTrackSubscribed(event: RoomEvent.TrackSubscribed) { | 135 | private fun onTrackSubscribed(event: RoomEvent.TrackSubscribed) { |
| 127 | - if (event.track is VideoTrack) { | 136 | + val track = event.track |
| 137 | + if (track is VideoTrack) { | ||
| 128 | attachVideo(track) | 138 | attachVideo(track) |
| 129 | } | 139 | } |
| 130 | } | 140 | } |
| 131 | 141 | ||
| 132 | private fun attachVideo(videoTrack: VideoTrack) { | 142 | private fun attachVideo(videoTrack: VideoTrack) { |
| 133 | - // viewBinding.renderer is a `io.livekit.android.renderer.SurfaceViewRenderer` in your | ||
| 134 | - // layout | ||
| 135 | - videoTrack.addRenderer(viewBinding.renderer) | 143 | + videoTrack.addRenderer(findViewById<SurfaceViewRenderer>(R.id.renderer)) |
| 144 | + findViewById<View>(R.id.progress).visibility = View.GONE | ||
| 136 | } | 145 | } |
| 137 | } | 146 | } |
| 138 | ``` | 147 | ``` |
| 139 | 148 | ||
| 149 | +See | ||
| 150 | +the [basic sample app](https://github.com/livekit/client-sdk-android/blob/main/sample-app-basic/src/main/java/io/livekit/android/sample/basic/MainActivity.kt) | ||
| 151 | +for the full implementation. | ||
| 152 | + | ||
| 140 | ### `@FlowObservable` | 153 | ### `@FlowObservable` |
| 141 | 154 | ||
| 142 | -Properties marked with `@FlowObservable` can be accessed as a Kotlin Flow to observe changes directly: | 155 | +Properties marked with `@FlowObservable` can be accessed as a Kotlin Flow to observe changes |
| 156 | +directly: | ||
| 143 | 157 | ||
| 144 | ```kt | 158 | ```kt |
| 145 | coroutineScope.launch { | 159 | coroutineScope.launch { |
| @@ -149,12 +163,33 @@ coroutineScope.launch { | @@ -149,12 +163,33 @@ coroutineScope.launch { | ||
| 149 | } | 163 | } |
| 150 | ``` | 164 | ``` |
| 151 | 165 | ||
| 166 | +## Sample App | ||
| 167 | + | ||
| 168 | +We have a basic quickstart sample | ||
| 169 | +app [here](https://github.com/livekit/client-sdk-android/blob/main/sample-app-basic), showing how to | ||
| 170 | +connect to a room, publish your device's audio/video, and display the video of one remote participant. | ||
| 171 | + | ||
| 172 | +There are two more full featured video conferencing sample apps: | ||
| 173 | + | ||
| 174 | +* [Compose app](https://github.com/livekit/client-sdk-android/tree/main/sample-app-compose/src/main/java/io/livekit/android/composesample) | ||
| 175 | +* [Standard app](https://github.com/livekit/client-sdk-android/tree/main/sample-app/src/main/java/io/livekit/android/sample) | ||
| 176 | + | ||
| 177 | +They both use | ||
| 178 | +the [`CallViewModel`](https://github.com/livekit/client-sdk-android/blob/main/sample-app-common/src/main/java/io/livekit/android/sample/CallViewModel.kt) | ||
| 179 | +, which handles the `Room` connection and exposes the data needed for a basic video conferencing | ||
| 180 | +app. | ||
| 181 | + | ||
| 182 | +The respective `ParticipantItem` class in each app is responsible for the displaying of each | ||
| 183 | +participant's UI. | ||
| 184 | + | ||
| 185 | +* [Compose `ParticipantItem`](https://github.com/livekit/client-sdk-android/blob/main/sample-app-compose/src/main/java/io/livekit/android/composesample/ParticipantItem.kt) | ||
| 186 | +* [Standard `ParticipantItem`](https://github.com/livekit/client-sdk-android/blob/main/sample-app/src/main/java/io/livekit/android/sample/ParticipantItem.kt) | ||
| 187 | + | ||
| 152 | ## Dev Environment | 188 | ## Dev Environment |
| 153 | 189 | ||
| 154 | -To develop the Android SDK or running the sample app, you'll need: | 190 | +To develop the Android SDK or running the sample app directly from this repo, you'll need: |
| 155 | 191 | ||
| 156 | - Ensure the protocol submodule repo is initialized and updated with `git submodule update --init` | 192 | - Ensure the protocol submodule repo is initialized and updated with `git submodule update --init` |
| 157 | -- Install [Android Studio Arctic Fox 2020.3.1+](https://developer.android.com/studio) | ||
| 158 | 193 | ||
| 159 | For those developing on Apple M1 Macs, please add below to $HOME/.gradle/gradle.properties | 194 | For those developing on Apple M1 Macs, please add below to $HOME/.gradle/gradle.properties |
| 160 | 195 |
| @@ -2,9 +2,9 @@ | @@ -2,9 +2,9 @@ | ||
| 2 | 2 | ||
| 3 | buildscript { | 3 | buildscript { |
| 4 | ext { | 4 | ext { |
| 5 | - compose_version = '1.1.1' | ||
| 6 | - compose_compiler_version = '1.1.1' | ||
| 7 | - kotlin_version = '1.6.10' | 5 | + compose_version = '1.2.0' |
| 6 | + compose_compiler_version = '1.2.0' | ||
| 7 | + kotlin_version = '1.7.0' | ||
| 8 | java_version = JavaVersion.VERSION_1_8 | 8 | java_version = JavaVersion.VERSION_1_8 |
| 9 | dokka_version = '1.5.0' | 9 | dokka_version = '1.5.0' |
| 10 | } | 10 | } |
| @@ -13,8 +13,8 @@ buildscript { | @@ -13,8 +13,8 @@ buildscript { | ||
| 13 | mavenCentral() | 13 | mavenCentral() |
| 14 | } | 14 | } |
| 15 | dependencies { | 15 | dependencies { |
| 16 | - classpath 'com.android.tools.build:gradle:7.1.2' | ||
| 17 | - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" | 16 | + classpath 'com.android.tools.build:gradle:7.1.3' |
| 17 | + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.0" | ||
| 18 | classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" | 18 | classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" |
| 19 | classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version" | 19 | classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version" |
| 20 | classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.18' | 20 | classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.18' |
| @@ -45,15 +45,15 @@ nexusStaging { | @@ -45,15 +45,15 @@ nexusStaging { | ||
| 45 | 45 | ||
| 46 | ext { | 46 | ext { |
| 47 | androidSdk = [ | 47 | androidSdk = [ |
| 48 | - compileVersion: 31, | ||
| 49 | - targetVersion : 31, | 48 | + compileVersion: 32, |
| 49 | + targetVersion : 32, | ||
| 50 | minVersion : 21, | 50 | minVersion : 21, |
| 51 | ] | 51 | ] |
| 52 | versions = [ | 52 | versions = [ |
| 53 | - androidx_core : "1.7.0", | ||
| 54 | - androidx_lifecycle: "2.4.0", | 53 | + androidx_core : "1.8.0", |
| 54 | + androidx_lifecycle: "2.5.1", | ||
| 55 | autoService : '1.0.1', | 55 | autoService : '1.0.1', |
| 56 | - dagger : "2.27", | 56 | + dagger : "2.43", |
| 57 | groupie : "2.9.0", | 57 | groupie : "2.9.0", |
| 58 | junit : "4.13.2", | 58 | junit : "4.13.2", |
| 59 | junitJupiter : "5.5.0", | 59 | junitJupiter : "5.5.0", |
| @@ -49,7 +49,7 @@ android { | @@ -49,7 +49,7 @@ android { | ||
| 49 | kotlinCompilerExtensionVersion compose_compiler_version | 49 | kotlinCompilerExtensionVersion compose_compiler_version |
| 50 | } | 50 | } |
| 51 | kotlinOptions { | 51 | kotlinOptions { |
| 52 | - freeCompilerArgs = ["-Xinline-classes", "-Xopt-in=kotlin.RequiresOptIn"] | 52 | + freeCompilerArgs = ["-Xinline-classes", "-opt-in=kotlin.RequiresOptIn"] |
| 53 | jvmTarget = java_version | 53 | jvmTarget = java_version |
| 54 | } | 54 | } |
| 55 | } | 55 | } |
| @@ -114,13 +114,13 @@ dependencies { | @@ -114,13 +114,13 @@ dependencies { | ||
| 114 | api 'com.github.webrtc-sdk:android:104.5112.01' | 114 | api 'com.github.webrtc-sdk:android:104.5112.01' |
| 115 | api "com.squareup.okhttp3:okhttp:4.10.0" | 115 | api "com.squareup.okhttp3:okhttp:4.10.0" |
| 116 | api "com.twilio:audioswitch:1.1.5" | 116 | api "com.twilio:audioswitch:1.1.5" |
| 117 | - implementation "androidx.annotation:annotation:1.3.0" | 117 | + implementation "androidx.annotation:annotation:1.4.0" |
| 118 | implementation "androidx.core:core:${versions.androidx_core}" | 118 | implementation "androidx.core:core:${versions.androidx_core}" |
| 119 | implementation "com.google.protobuf:protobuf-javalite:${versions.protobuf}" | 119 | implementation "com.google.protobuf:protobuf-javalite:${versions.protobuf}" |
| 120 | implementation "androidx.compose.ui:ui:$compose_version" | 120 | implementation "androidx.compose.ui:ui:$compose_version" |
| 121 | 121 | ||
| 122 | - implementation 'com.google.dagger:dagger:2.38' | ||
| 123 | - kapt 'com.google.dagger:dagger-compiler:2.38' | 122 | + implementation "com.google.dagger:dagger:${versions.dagger}" |
| 123 | + kapt "com.google.dagger:dagger-compiler:${versions.dagger}" | ||
| 124 | 124 | ||
| 125 | implementation deps.timber | 125 | implementation deps.timber |
| 126 | implementation 'com.vdurmont:semver4j:3.1.0' | 126 | implementation 'com.vdurmont:semver4j:3.1.0' |
| @@ -6,6 +6,15 @@ Android Client SDK to [LiveKit](https://github.com/livekit/livekit-server). | @@ -6,6 +6,15 @@ Android Client SDK to [LiveKit](https://github.com/livekit/livekit-server). | ||
| 6 | 6 | ||
| 7 | This package contains the initial `connect` function. | 7 | This package contains the initial `connect` function. |
| 8 | 8 | ||
| 9 | +# Package io.livekit.android.compose | ||
| 10 | + | ||
| 11 | +Utilities and composables for use with Jetpack Compose. | ||
| 12 | + | ||
| 9 | # Package io.livekit.android.room | 13 | # Package io.livekit.android.room |
| 10 | 14 | ||
| 11 | -Room is the primary class that manages the connection to the LiveKit Room. It exposes listeners that lets you hook into room events | 15 | +Room is the primary class that manages the connection to the LiveKit Room. It exposes listeners that lets you hook into room events. |
| 16 | + | ||
| 17 | +# Package io.livekit.android.room.track | ||
| 18 | + | ||
| 19 | +`AudioTrack` and `VideoTrack` are the classes that represent the types of media streams that can be | ||
| 20 | +subscribed and published. |
| @@ -10,13 +10,43 @@ import com.twilio.audioswitch.AudioSwitch | @@ -10,13 +10,43 @@ import com.twilio.audioswitch.AudioSwitch | ||
| 10 | import javax.inject.Inject | 10 | import javax.inject.Inject |
| 11 | import javax.inject.Singleton | 11 | import javax.inject.Singleton |
| 12 | 12 | ||
| 13 | +/** | ||
| 14 | + * An [AudioHandler] built on top of [AudioSwitch]. | ||
| 15 | + * | ||
| 16 | + * The various settings should be set before connecting to a [Room] and [start] is called. | ||
| 17 | + */ | ||
| 13 | @Singleton | 18 | @Singleton |
| 14 | class AudioSwitchHandler | 19 | class AudioSwitchHandler |
| 15 | @Inject | 20 | @Inject |
| 16 | constructor(private val context: Context) : AudioHandler { | 21 | constructor(private val context: Context) : AudioHandler { |
| 22 | + /** | ||
| 23 | + * Toggle whether logging is enabled for [AudioSwitch]. By default, this is set to false. | ||
| 24 | + */ | ||
| 17 | var loggingEnabled = false | 25 | var loggingEnabled = false |
| 26 | + | ||
| 27 | + /** | ||
| 28 | + * Listen to changes in the available and active audio devices. | ||
| 29 | + * | ||
| 30 | + * @see AudioDeviceChangeListener | ||
| 31 | + */ | ||
| 18 | var audioDeviceChangeListener: AudioDeviceChangeListener? = null | 32 | var audioDeviceChangeListener: AudioDeviceChangeListener? = null |
| 33 | + | ||
| 34 | + /** | ||
| 35 | + * Listen to changes in audio focus. | ||
| 36 | + * | ||
| 37 | + * @see AudioManager.OnAudioFocusChangeListener | ||
| 38 | + */ | ||
| 19 | var onAudioFocusChangeListener: AudioManager.OnAudioFocusChangeListener? = null | 39 | var onAudioFocusChangeListener: AudioManager.OnAudioFocusChangeListener? = null |
| 40 | + | ||
| 41 | + /** | ||
| 42 | + * The preferred priority of audio devices to use. The first available audio device will be used. | ||
| 43 | + * | ||
| 44 | + * By default, the preferred order is set to: | ||
| 45 | + * 1. BluetoothHeadset | ||
| 46 | + * 2. WiredHeadset | ||
| 47 | + * 3. Earpiece | ||
| 48 | + * 4. Speakerphone | ||
| 49 | + */ | ||
| 20 | var preferredDeviceList: List<Class<out AudioDevice>>? = null | 50 | var preferredDeviceList: List<Class<out AudioDevice>>? = null |
| 21 | 51 | ||
| 22 | private var audioSwitch: AudioSwitch? = null | 52 | private var audioSwitch: AudioSwitch? = null |
| @@ -602,6 +602,7 @@ internal constructor( | @@ -602,6 +602,7 @@ internal constructor( | ||
| 602 | LKLog.e { "error setting remote description for answer: ${outcome.value} " } | 602 | LKLog.e { "error setting remote description for answer: ${outcome.value} " } |
| 603 | return@launch | 603 | return@launch |
| 604 | } | 604 | } |
| 605 | + else -> {} | ||
| 605 | } | 606 | } |
| 606 | } | 607 | } |
| 607 | 608 | ||
| @@ -621,6 +622,7 @@ internal constructor( | @@ -621,6 +622,7 @@ internal constructor( | ||
| 621 | LKLog.e { "error setting local description for answer: ${outcome.value}" } | 622 | LKLog.e { "error setting local description for answer: ${outcome.value}" } |
| 622 | return@launch | 623 | return@launch |
| 623 | } | 624 | } |
| 625 | + else -> {} | ||
| 624 | } | 626 | } |
| 625 | } | 627 | } |
| 626 | 628 |
| @@ -45,7 +45,7 @@ constructor( | @@ -45,7 +45,7 @@ constructor( | ||
| 45 | @Named(InjectionNames.DISPATCHER_IO) | 45 | @Named(InjectionNames.DISPATCHER_IO) |
| 46 | private val ioDispatcher: CoroutineDispatcher, | 46 | private val ioDispatcher: CoroutineDispatcher, |
| 47 | val audioHandler: AudioHandler, | 47 | val audioHandler: AudioHandler, |
| 48 | -) : RTCEngine.Listener, ParticipantListener, ConnectivityManager.NetworkCallback() { | 48 | +) : RTCEngine.Listener, ParticipantListener { |
| 49 | 49 | ||
| 50 | private lateinit var coroutineScope: CoroutineScope | 50 | private lateinit var coroutineScope: CoroutineScope |
| 51 | private val eventBus = BroadcastEventBus<RoomEvent>() | 51 | private val eventBus = BroadcastEventBus<RoomEvent>() |
| @@ -217,7 +217,7 @@ constructor( | @@ -217,7 +217,7 @@ constructor( | ||
| 217 | val networkRequest = NetworkRequest.Builder() | 217 | val networkRequest = NetworkRequest.Builder() |
| 218 | .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) | 218 | .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) |
| 219 | .build() | 219 | .build() |
| 220 | - cm.registerNetworkCallback(networkRequest, this) | 220 | + cm.registerNetworkCallback(networkRequest, networkCallback) |
| 221 | 221 | ||
| 222 | if (options.audio) { | 222 | if (options.audio) { |
| 223 | val audioTrack = localParticipant.createAudioTrack() | 223 | val audioTrack = localParticipant.createAudioTrack() |
| @@ -237,6 +237,9 @@ constructor( | @@ -237,6 +237,9 @@ constructor( | ||
| 237 | handleDisconnect() | 237 | handleDisconnect() |
| 238 | } | 238 | } |
| 239 | 239 | ||
| 240 | + /** | ||
| 241 | + * @suppress | ||
| 242 | + */ | ||
| 240 | override fun onJoinResponse(response: LivekitRtc.JoinResponse) { | 243 | override fun onJoinResponse(response: LivekitRtc.JoinResponse) { |
| 241 | 244 | ||
| 242 | LKLog.i { "Connected to server, server version: ${response.serverVersion}, client version: ${Version.CLIENT_VERSION}" } | 245 | LKLog.i { "Connected to server, server version: ${response.serverVersion}, client version: ${Version.CLIENT_VERSION}" } |
| @@ -441,7 +444,7 @@ constructor( | @@ -441,7 +444,7 @@ constructor( | ||
| 441 | 444 | ||
| 442 | try { | 445 | try { |
| 443 | val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager | 446 | val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager |
| 444 | - cm.unregisterNetworkCallback(this) | 447 | + cm.unregisterNetworkCallback(networkCallback) |
| 445 | } catch (e: IllegalArgumentException) { | 448 | } catch (e: IllegalArgumentException) { |
| 446 | // do nothing, may happen on older versions if attempting to unregister twice. | 449 | // do nothing, may happen on older versions if attempting to unregister twice. |
| 447 | } | 450 | } |
| @@ -512,29 +515,29 @@ constructor( | @@ -512,29 +515,29 @@ constructor( | ||
| 512 | } | 515 | } |
| 513 | 516 | ||
| 514 | //------------------------------------- NetworkCallback -------------------------------------// | 517 | //------------------------------------- NetworkCallback -------------------------------------// |
| 515 | - | ||
| 516 | - /** | ||
| 517 | - * @suppress | ||
| 518 | - */ | ||
| 519 | - override fun onLost(network: Network) { | ||
| 520 | - // lost connection, flip to reconnecting | ||
| 521 | - hasLostConnectivity = true | ||
| 522 | - } | ||
| 523 | - | ||
| 524 | - /** | ||
| 525 | - * @suppress | ||
| 526 | - */ | ||
| 527 | - override fun onAvailable(network: Network) { | ||
| 528 | - // only actually reconnect after connection is re-established | ||
| 529 | - if (!hasLostConnectivity) { | ||
| 530 | - return | 518 | + private val networkCallback = object : ConnectivityManager.NetworkCallback() { |
| 519 | + /** | ||
| 520 | + * @suppress | ||
| 521 | + */ | ||
| 522 | + override fun onLost(network: Network) { | ||
| 523 | + // lost connection, flip to reconnecting | ||
| 524 | + hasLostConnectivity = true | ||
| 525 | + } | ||
| 526 | + | ||
| 527 | + /** | ||
| 528 | + * @suppress | ||
| 529 | + */ | ||
| 530 | + override fun onAvailable(network: Network) { | ||
| 531 | + // only actually reconnect after connection is re-established | ||
| 532 | + if (!hasLostConnectivity) { | ||
| 533 | + return | ||
| 534 | + } | ||
| 535 | + LKLog.i { "network connection available, reconnecting" } | ||
| 536 | + reconnect() | ||
| 537 | + hasLostConnectivity = false | ||
| 531 | } | 538 | } |
| 532 | - LKLog.i { "network connection available, reconnecting" } | ||
| 533 | - reconnect() | ||
| 534 | - hasLostConnectivity = false | ||
| 535 | } | 539 | } |
| 536 | 540 | ||
| 537 | - | ||
| 538 | //----------------------------------- RTCEngine.Listener ------------------------------------// | 541 | //----------------------------------- RTCEngine.Listener ------------------------------------// |
| 539 | 542 | ||
| 540 | /** | 543 | /** |
| @@ -820,7 +823,6 @@ constructor( | @@ -820,7 +823,6 @@ constructor( | ||
| 820 | } | 823 | } |
| 821 | 824 | ||
| 822 | /** | 825 | /** |
| 823 | - * @suppress | ||
| 824 | * // TODO(@dl): can this be moved out of Room/SDK? | 826 | * // TODO(@dl): can this be moved out of Room/SDK? |
| 825 | */ | 827 | */ |
| 826 | fun initVideoRenderer(viewRenderer: SurfaceViewRenderer) { | 828 | fun initVideoRenderer(viewRenderer: SurfaceViewRenderer) { |
| @@ -830,7 +832,6 @@ constructor( | @@ -830,7 +832,6 @@ constructor( | ||
| 830 | } | 832 | } |
| 831 | 833 | ||
| 832 | /** | 834 | /** |
| 833 | - * @suppress | ||
| 834 | * // TODO(@dl): can this be moved out of Room/SDK? | 835 | * // TODO(@dl): can this be moved out of Room/SDK? |
| 835 | */ | 836 | */ |
| 836 | fun initVideoRenderer(viewRenderer: TextureViewRenderer) { | 837 | fun initVideoRenderer(viewRenderer: TextureViewRenderer) { |
| @@ -180,6 +180,9 @@ internal constructor( | @@ -180,6 +180,9 @@ internal constructor( | ||
| 180 | createScreencastTrack(mediaProjectionPermissionResultData = mediaProjectionPermissionResultData) | 180 | createScreencastTrack(mediaProjectionPermissionResultData = mediaProjectionPermissionResultData) |
| 181 | publishVideoTrack(track) | 181 | publishVideoTrack(track) |
| 182 | } | 182 | } |
| 183 | + else -> { | ||
| 184 | + LKLog.w { "Attempting to enable an unknown source, ignoring." } | ||
| 185 | + } | ||
| 183 | } | 186 | } |
| 184 | } | 187 | } |
| 185 | } else { | 188 | } else { |
| 1 | package io.livekit.android.room | 1 | package io.livekit.android.room |
| 2 | 2 | ||
| 3 | +import android.content.Context | ||
| 4 | +import android.net.ConnectivityManager | ||
| 3 | import android.net.Network | 5 | import android.net.Network |
| 6 | +import androidx.test.platform.app.InstrumentationRegistry | ||
| 4 | import io.livekit.android.MockE2ETest | 7 | import io.livekit.android.MockE2ETest |
| 5 | import io.livekit.android.events.EventCollector | 8 | import io.livekit.android.events.EventCollector |
| 6 | import io.livekit.android.events.FlowCollector | 9 | import io.livekit.android.events.FlowCollector |
| @@ -23,6 +26,9 @@ import org.junit.Test | @@ -23,6 +26,9 @@ import org.junit.Test | ||
| 23 | import org.junit.runner.RunWith | 26 | import org.junit.runner.RunWith |
| 24 | import org.mockito.Mockito | 27 | import org.mockito.Mockito |
| 25 | import org.robolectric.RobolectricTestRunner | 28 | import org.robolectric.RobolectricTestRunner |
| 29 | +import org.robolectric.Shadows.shadowOf | ||
| 30 | +import org.robolectric.shadows.ShadowConnectivityManager | ||
| 31 | +import org.robolectric.shadows.ShadowNetworkInfo | ||
| 26 | 32 | ||
| 27 | @ExperimentalCoroutinesApi | 33 | @ExperimentalCoroutinesApi |
| 28 | @RunWith(RobolectricTestRunner::class) | 34 | @RunWith(RobolectricTestRunner::class) |
| @@ -259,8 +265,19 @@ class RoomMockE2ETest : MockE2ETest() { | @@ -259,8 +265,19 @@ class RoomMockE2ETest : MockE2ETest() { | ||
| 259 | connect() | 265 | connect() |
| 260 | val eventCollector = EventCollector(room.events, coroutineRule.scope) | 266 | val eventCollector = EventCollector(room.events, coroutineRule.scope) |
| 261 | val network = Mockito.mock(Network::class.java) | 267 | val network = Mockito.mock(Network::class.java) |
| 262 | - room.onLost(network) | ||
| 263 | - room.onAvailable(network) | 268 | + |
| 269 | + val connectivityManager = InstrumentationRegistry.getInstrumentation() | ||
| 270 | + .context | ||
| 271 | + .getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager | ||
| 272 | + val shadowConnectivityManager: ShadowConnectivityManager = shadowOf(connectivityManager) | ||
| 273 | + | ||
| 274 | + shadowConnectivityManager.networkCallbacks.forEach { callback -> | ||
| 275 | + callback.onLost(network) | ||
| 276 | + } | ||
| 277 | + shadowConnectivityManager.networkCallbacks.forEach { callback -> | ||
| 278 | + callback.onAvailable(network) | ||
| 279 | + } | ||
| 280 | + | ||
| 264 | val events = eventCollector.stopCollecting() | 281 | val events = eventCollector.stopCollecting() |
| 265 | 282 | ||
| 266 | Assert.assertEquals(1, events.size) | 283 | Assert.assertEquals(1, events.size) |
| 1 | package io.livekit.android.room | 1 | package io.livekit.android.room |
| 2 | 2 | ||
| 3 | import android.content.Context | 3 | import android.content.Context |
| 4 | +import android.net.ConnectivityManager | ||
| 4 | import android.net.Network | 5 | import android.net.Network |
| 5 | import androidx.test.core.app.ApplicationProvider | 6 | import androidx.test.core.app.ApplicationProvider |
| 7 | +import androidx.test.platform.app.InstrumentationRegistry | ||
| 6 | import io.livekit.android.audio.NoAudioHandler | 8 | import io.livekit.android.audio.NoAudioHandler |
| 7 | import io.livekit.android.coroutines.TestCoroutineRule | 9 | import io.livekit.android.coroutines.TestCoroutineRule |
| 8 | import io.livekit.android.events.EventCollector | 10 | import io.livekit.android.events.EventCollector |
| @@ -26,6 +28,8 @@ import org.mockito.Mockito | @@ -26,6 +28,8 @@ import org.mockito.Mockito | ||
| 26 | import org.mockito.junit.MockitoJUnit | 28 | import org.mockito.junit.MockitoJUnit |
| 27 | import org.mockito.kotlin.* | 29 | import org.mockito.kotlin.* |
| 28 | import org.robolectric.RobolectricTestRunner | 30 | import org.robolectric.RobolectricTestRunner |
| 31 | +import org.robolectric.Shadows | ||
| 32 | +import org.robolectric.shadows.ShadowConnectivityManager | ||
| 29 | import org.webrtc.EglBase | 33 | import org.webrtc.EglBase |
| 30 | 34 | ||
| 31 | @ExperimentalCoroutinesApi | 35 | @ExperimentalCoroutinesApi |
| @@ -108,8 +112,19 @@ class RoomTest { | @@ -108,8 +112,19 @@ class RoomTest { | ||
| 108 | connect() | 112 | connect() |
| 109 | 113 | ||
| 110 | val network = Mockito.mock(Network::class.java) | 114 | val network = Mockito.mock(Network::class.java) |
| 111 | - room.onLost(network) | ||
| 112 | - room.onAvailable(network) | 115 | + |
| 116 | + val connectivityManager = InstrumentationRegistry.getInstrumentation() | ||
| 117 | + .context | ||
| 118 | + .getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager | ||
| 119 | + val shadowConnectivityManager: ShadowConnectivityManager = Shadows.shadowOf(connectivityManager) | ||
| 120 | + | ||
| 121 | + shadowConnectivityManager.networkCallbacks.forEach { callback -> | ||
| 122 | + callback.onLost(network) | ||
| 123 | + } | ||
| 124 | + shadowConnectivityManager.networkCallbacks.forEach { callback -> | ||
| 125 | + callback.onAvailable(network) | ||
| 126 | + } | ||
| 127 | + | ||
| 113 | Mockito.verify(rtcEngine).reconnect() | 128 | Mockito.verify(rtcEngine).reconnect() |
| 114 | } | 129 | } |
| 115 | 130 |
sample-app-basic/.gitignore
0 → 100644
| 1 | +/build |
sample-app-basic/README.md
0 → 100644
| 1 | +# Sample-app-basic | ||
| 2 | + | ||
| 3 | +A quickstart app showing how to: | ||
| 4 | + | ||
| 5 | +1. Connect to a room. | ||
| 6 | +2. Publish your device's audio/video. | ||
| 7 | +3. Display a remote participant's video. | ||
| 8 | + | ||
| 9 | +This app only handles the video of one remote participant. For a more fully featured video | ||
| 10 | +conferencing app, check out the other sample apps in this repo. |
sample-app-basic/build.gradle
0 → 100644
| 1 | +plugins { | ||
| 2 | + id 'com.android.application' | ||
| 3 | + id 'org.jetbrains.kotlin.android' | ||
| 4 | +} | ||
| 5 | + | ||
| 6 | +android { | ||
| 7 | + compileSdk 32 | ||
| 8 | + | ||
| 9 | + defaultConfig { | ||
| 10 | + applicationId "io.livekit.android.sample.basic" | ||
| 11 | + minSdk 21 | ||
| 12 | + targetSdk 32 | ||
| 13 | + versionCode 1 | ||
| 14 | + versionName "1.0" | ||
| 15 | + | ||
| 16 | + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" | ||
| 17 | + } | ||
| 18 | + | ||
| 19 | + buildTypes { | ||
| 20 | + release { | ||
| 21 | + minifyEnabled false | ||
| 22 | + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' | ||
| 23 | + } | ||
| 24 | + } | ||
| 25 | + compileOptions { | ||
| 26 | + sourceCompatibility JavaVersion.VERSION_1_8 | ||
| 27 | + targetCompatibility JavaVersion.VERSION_1_8 | ||
| 28 | + } | ||
| 29 | + kotlinOptions { | ||
| 30 | + jvmTarget = '1.8' | ||
| 31 | + } | ||
| 32 | +} | ||
| 33 | + | ||
| 34 | +dependencies { | ||
| 35 | + | ||
| 36 | + implementation "androidx.core:core-ktx:${versions.androidx_core}" | ||
| 37 | + implementation 'androidx.appcompat:appcompat:1.4.2' | ||
| 38 | + implementation "androidx.activity:activity-ktx:1.5.1" | ||
| 39 | + implementation 'androidx.fragment:fragment-ktx:1.5.1' | ||
| 40 | + implementation "androidx.viewpager2:viewpager2:1.0.0" | ||
| 41 | + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${versions.androidx_lifecycle}" | ||
| 42 | + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${versions.androidx_lifecycle}" | ||
| 43 | + implementation "androidx.lifecycle:lifecycle-common-java8:${versions.androidx_lifecycle}" | ||
| 44 | + implementation 'com.google.android.material:material:1.6.1' | ||
| 45 | + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' | ||
| 46 | + implementation project(":livekit-android-sdk") | ||
| 47 | + testImplementation 'junit:junit:4.13.2' | ||
| 48 | + androidTestImplementation 'androidx.test.ext:junit:1.1.3' | ||
| 49 | + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' | ||
| 50 | +} |
sample-app-basic/proguard-rules.pro
0 → 100644
| 1 | +# Add project specific ProGuard rules here. | ||
| 2 | +# You can control the set of applied configuration files using the | ||
| 3 | +# proguardFiles setting in build.gradle. | ||
| 4 | +# | ||
| 5 | +# For more details, see | ||
| 6 | +# http://developer.android.com/guide/developing/tools/proguard.html | ||
| 7 | + | ||
| 8 | +# If your project uses WebView with JS, uncomment the following | ||
| 9 | +# and specify the fully qualified class name to the JavaScript interface | ||
| 10 | +# class: | ||
| 11 | +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { | ||
| 12 | +# public *; | ||
| 13 | +#} | ||
| 14 | + | ||
| 15 | +# Uncomment this to preserve the line number information for | ||
| 16 | +# debugging stack traces. | ||
| 17 | +#-keepattributes SourceFile,LineNumberTable | ||
| 18 | + | ||
| 19 | +# If you keep the line number information, uncomment this to | ||
| 20 | +# hide the original source file name. | ||
| 21 | +#-renamesourcefileattribute SourceFile |
sample-app-basic/src/androidTest/java/io/livekit/android/sample/basic/ExampleInstrumentedTest.kt
0 → 100644
| 1 | +package io.livekit.android.sample.basic | ||
| 2 | + | ||
| 3 | +import androidx.test.platform.app.InstrumentationRegistry | ||
| 4 | +import androidx.test.ext.junit.runners.AndroidJUnit4 | ||
| 5 | + | ||
| 6 | +import org.junit.Test | ||
| 7 | +import org.junit.runner.RunWith | ||
| 8 | + | ||
| 9 | +import org.junit.Assert.* | ||
| 10 | + | ||
| 11 | +/** | ||
| 12 | + * Instrumented test, which will execute on an Android device. | ||
| 13 | + * | ||
| 14 | + * See [testing documentation](http://d.android.com/tools/testing). | ||
| 15 | + */ | ||
| 16 | +@RunWith(AndroidJUnit4::class) | ||
| 17 | +class ExampleInstrumentedTest { | ||
| 18 | + @Test | ||
| 19 | + fun useAppContext() { | ||
| 20 | + // Context of the app under test. | ||
| 21 | + val appContext = InstrumentationRegistry.getInstrumentation().targetContext | ||
| 22 | + assertEquals("io.livekit.android.sample.basic", appContext.packageName) | ||
| 23 | + } | ||
| 24 | +} |
| 1 | +<?xml version="1.0" encoding="utf-8"?> | ||
| 2 | +<manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 3 | + package="io.livekit.android.sample.basic"> | ||
| 4 | + | ||
| 5 | + <application | ||
| 6 | + android:allowBackup="true" | ||
| 7 | + android:icon="@mipmap/ic_launcher" | ||
| 8 | + android:label="@string/app_name" | ||
| 9 | + android:networkSecurityConfig="@xml/network_security_config" | ||
| 10 | + android:roundIcon="@mipmap/ic_launcher_round" | ||
| 11 | + android:supportsRtl="true" | ||
| 12 | + android:theme="@style/Theme.Livekitandroid"> | ||
| 13 | + <activity | ||
| 14 | + android:name=".MainActivity" | ||
| 15 | + android:exported="true"> | ||
| 16 | + <intent-filter> | ||
| 17 | + <action android:name="android.intent.action.MAIN" /> | ||
| 18 | + | ||
| 19 | + <category android:name="android.intent.category.LAUNCHER" /> | ||
| 20 | + </intent-filter> | ||
| 21 | + </activity> | ||
| 22 | + </application> | ||
| 23 | + | ||
| 24 | +</manifest> |
| 1 | +package io.livekit.android.sample.basic | ||
| 2 | + | ||
| 3 | +import android.Manifest | ||
| 4 | +import android.content.pm.PackageManager | ||
| 5 | +import android.os.Build | ||
| 6 | +import android.os.Bundle | ||
| 7 | +import android.view.View | ||
| 8 | +import android.widget.Toast | ||
| 9 | +import androidx.activity.result.contract.ActivityResultContracts | ||
| 10 | +import androidx.appcompat.app.AppCompatActivity | ||
| 11 | +import androidx.core.content.ContextCompat | ||
| 12 | +import androidx.lifecycle.lifecycleScope | ||
| 13 | +import io.livekit.android.LiveKit | ||
| 14 | +import io.livekit.android.events.RoomEvent | ||
| 15 | +import io.livekit.android.events.collect | ||
| 16 | +import io.livekit.android.renderer.SurfaceViewRenderer | ||
| 17 | +import io.livekit.android.room.Room | ||
| 18 | +import io.livekit.android.room.track.VideoTrack | ||
| 19 | +import kotlinx.coroutines.launch | ||
| 20 | + | ||
| 21 | +class MainActivity : AppCompatActivity() { | ||
| 22 | + | ||
| 23 | + lateinit var room: Room | ||
| 24 | + | ||
| 25 | + override fun onCreate(savedInstanceState: Bundle?) { | ||
| 26 | + super.onCreate(savedInstanceState) | ||
| 27 | + | ||
| 28 | + setContentView(R.layout.activity_main) | ||
| 29 | + | ||
| 30 | + // Create Room object. | ||
| 31 | + room = LiveKit.create(applicationContext) | ||
| 32 | + | ||
| 33 | + // Setup the video renderer | ||
| 34 | + room.initVideoRenderer(findViewById<SurfaceViewRenderer>(R.id.renderer)) | ||
| 35 | + | ||
| 36 | + requestNeededPermissions { connectToRoom() } | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + private fun connectToRoom() { | ||
| 40 | + | ||
| 41 | + val url = "wss://your_host" | ||
| 42 | + val token = "your_token" | ||
| 43 | + | ||
| 44 | + lifecycleScope.launch { | ||
| 45 | + | ||
| 46 | + // Setup event handling. | ||
| 47 | + launch { | ||
| 48 | + room.events.collect { event -> | ||
| 49 | + when (event) { | ||
| 50 | + is RoomEvent.TrackSubscribed -> onTrackSubscribed(event) | ||
| 51 | + else -> {} | ||
| 52 | + } | ||
| 53 | + } | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + // Connect to server. | ||
| 57 | + room.connect( | ||
| 58 | + url, | ||
| 59 | + token, | ||
| 60 | + ) | ||
| 61 | + | ||
| 62 | + // Turn on audio/video recording. | ||
| 63 | + val localParticipant = room.localParticipant | ||
| 64 | + localParticipant.setMicrophoneEnabled(true) | ||
| 65 | + localParticipant.setCameraEnabled(true) | ||
| 66 | + } | ||
| 67 | + } | ||
| 68 | + | ||
| 69 | + private fun onTrackSubscribed(event: RoomEvent.TrackSubscribed) { | ||
| 70 | + val track = event.track | ||
| 71 | + if (track is VideoTrack) { | ||
| 72 | + attachVideo(track) | ||
| 73 | + } | ||
| 74 | + } | ||
| 75 | + | ||
| 76 | + private fun attachVideo(videoTrack: VideoTrack) { | ||
| 77 | + videoTrack.addRenderer(findViewById<SurfaceViewRenderer>(R.id.renderer)) | ||
| 78 | + findViewById<View>(R.id.progress).visibility = View.GONE | ||
| 79 | + } | ||
| 80 | + | ||
| 81 | + private fun requestNeededPermissions(onHasPermissions: () -> Unit) { | ||
| 82 | + val requestPermissionLauncher = | ||
| 83 | + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { grants -> | ||
| 84 | + var hasDenied = false | ||
| 85 | + // Check if any permissions weren't granted. | ||
| 86 | + for (grant in grants.entries) { | ||
| 87 | + if (!grant.value) { | ||
| 88 | + Toast.makeText(this, "Missing permission: ${grant.key}", Toast.LENGTH_SHORT).show() | ||
| 89 | + | ||
| 90 | + hasDenied = true | ||
| 91 | + } | ||
| 92 | + } | ||
| 93 | + | ||
| 94 | + if (!hasDenied) { | ||
| 95 | + onHasPermissions() | ||
| 96 | + } | ||
| 97 | + } | ||
| 98 | + | ||
| 99 | + // Assemble the needed permissions to request | ||
| 100 | + val neededPermissions = listOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA) | ||
| 101 | + .let { perms -> | ||
| 102 | + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { | ||
| 103 | + // Need BLUETOOTH_CONNECT permission on API S+ to output to bluetooth devices. | ||
| 104 | + perms + listOf(Manifest.permission.BLUETOOTH_CONNECT) | ||
| 105 | + } else { | ||
| 106 | + perms | ||
| 107 | + } | ||
| 108 | + } | ||
| 109 | + .filter { ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_DENIED } | ||
| 110 | + .toTypedArray() | ||
| 111 | + | ||
| 112 | + if (neededPermissions.isNotEmpty()) { | ||
| 113 | + requestPermissionLauncher.launch(neededPermissions) | ||
| 114 | + } else { | ||
| 115 | + onHasPermissions() | ||
| 116 | + } | ||
| 117 | + } | ||
| 118 | + | ||
| 119 | + override fun onDestroy() { | ||
| 120 | + super.onDestroy() | ||
| 121 | + room.disconnect() | ||
| 122 | + } | ||
| 123 | +} |
| 1 | +<vector xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 2 | + xmlns:aapt="http://schemas.android.com/aapt" | ||
| 3 | + android:width="108dp" | ||
| 4 | + android:height="108dp" | ||
| 5 | + android:viewportWidth="108" | ||
| 6 | + android:viewportHeight="108"> | ||
| 7 | + <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"> | ||
| 8 | + <aapt:attr name="android:fillColor"> | ||
| 9 | + <gradient | ||
| 10 | + android:endX="85.84757" | ||
| 11 | + android:endY="92.4963" | ||
| 12 | + android:startX="42.9492" | ||
| 13 | + android:startY="49.59793" | ||
| 14 | + android:type="linear"> | ||
| 15 | + <item | ||
| 16 | + android:color="#44000000" | ||
| 17 | + android:offset="0.0" /> | ||
| 18 | + <item | ||
| 19 | + android:color="#00000000" | ||
| 20 | + android:offset="1.0" /> | ||
| 21 | + </gradient> | ||
| 22 | + </aapt:attr> | ||
| 23 | + </path> | ||
| 24 | + <path | ||
| 25 | + android:fillColor="#FFFFFF" | ||
| 26 | + android:fillType="nonZero" | ||
| 27 | + 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" | ||
| 28 | + android:strokeWidth="1" | ||
| 29 | + android:strokeColor="#00000000" /> | ||
| 30 | +</vector> |
| 1 | +<?xml version="1.0" encoding="utf-8"?> | ||
| 2 | +<vector xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 3 | + android:width="108dp" | ||
| 4 | + android:height="108dp" | ||
| 5 | + android:viewportWidth="108" | ||
| 6 | + android:viewportHeight="108"> | ||
| 7 | + <path | ||
| 8 | + android:fillColor="#3DDC84" | ||
| 9 | + android:pathData="M0,0h108v108h-108z" /> | ||
| 10 | + <path | ||
| 11 | + android:fillColor="#00000000" | ||
| 12 | + android:pathData="M9,0L9,108" | ||
| 13 | + android:strokeWidth="0.8" | ||
| 14 | + android:strokeColor="#33FFFFFF" /> | ||
| 15 | + <path | ||
| 16 | + android:fillColor="#00000000" | ||
| 17 | + android:pathData="M19,0L19,108" | ||
| 18 | + android:strokeWidth="0.8" | ||
| 19 | + android:strokeColor="#33FFFFFF" /> | ||
| 20 | + <path | ||
| 21 | + android:fillColor="#00000000" | ||
| 22 | + android:pathData="M29,0L29,108" | ||
| 23 | + android:strokeWidth="0.8" | ||
| 24 | + android:strokeColor="#33FFFFFF" /> | ||
| 25 | + <path | ||
| 26 | + android:fillColor="#00000000" | ||
| 27 | + android:pathData="M39,0L39,108" | ||
| 28 | + android:strokeWidth="0.8" | ||
| 29 | + android:strokeColor="#33FFFFFF" /> | ||
| 30 | + <path | ||
| 31 | + android:fillColor="#00000000" | ||
| 32 | + android:pathData="M49,0L49,108" | ||
| 33 | + android:strokeWidth="0.8" | ||
| 34 | + android:strokeColor="#33FFFFFF" /> | ||
| 35 | + <path | ||
| 36 | + android:fillColor="#00000000" | ||
| 37 | + android:pathData="M59,0L59,108" | ||
| 38 | + android:strokeWidth="0.8" | ||
| 39 | + android:strokeColor="#33FFFFFF" /> | ||
| 40 | + <path | ||
| 41 | + android:fillColor="#00000000" | ||
| 42 | + android:pathData="M69,0L69,108" | ||
| 43 | + android:strokeWidth="0.8" | ||
| 44 | + android:strokeColor="#33FFFFFF" /> | ||
| 45 | + <path | ||
| 46 | + android:fillColor="#00000000" | ||
| 47 | + android:pathData="M79,0L79,108" | ||
| 48 | + android:strokeWidth="0.8" | ||
| 49 | + android:strokeColor="#33FFFFFF" /> | ||
| 50 | + <path | ||
| 51 | + android:fillColor="#00000000" | ||
| 52 | + android:pathData="M89,0L89,108" | ||
| 53 | + android:strokeWidth="0.8" | ||
| 54 | + android:strokeColor="#33FFFFFF" /> | ||
| 55 | + <path | ||
| 56 | + android:fillColor="#00000000" | ||
| 57 | + android:pathData="M99,0L99,108" | ||
| 58 | + android:strokeWidth="0.8" | ||
| 59 | + android:strokeColor="#33FFFFFF" /> | ||
| 60 | + <path | ||
| 61 | + android:fillColor="#00000000" | ||
| 62 | + android:pathData="M0,9L108,9" | ||
| 63 | + android:strokeWidth="0.8" | ||
| 64 | + android:strokeColor="#33FFFFFF" /> | ||
| 65 | + <path | ||
| 66 | + android:fillColor="#00000000" | ||
| 67 | + android:pathData="M0,19L108,19" | ||
| 68 | + android:strokeWidth="0.8" | ||
| 69 | + android:strokeColor="#33FFFFFF" /> | ||
| 70 | + <path | ||
| 71 | + android:fillColor="#00000000" | ||
| 72 | + android:pathData="M0,29L108,29" | ||
| 73 | + android:strokeWidth="0.8" | ||
| 74 | + android:strokeColor="#33FFFFFF" /> | ||
| 75 | + <path | ||
| 76 | + android:fillColor="#00000000" | ||
| 77 | + android:pathData="M0,39L108,39" | ||
| 78 | + android:strokeWidth="0.8" | ||
| 79 | + android:strokeColor="#33FFFFFF" /> | ||
| 80 | + <path | ||
| 81 | + android:fillColor="#00000000" | ||
| 82 | + android:pathData="M0,49L108,49" | ||
| 83 | + android:strokeWidth="0.8" | ||
| 84 | + android:strokeColor="#33FFFFFF" /> | ||
| 85 | + <path | ||
| 86 | + android:fillColor="#00000000" | ||
| 87 | + android:pathData="M0,59L108,59" | ||
| 88 | + android:strokeWidth="0.8" | ||
| 89 | + android:strokeColor="#33FFFFFF" /> | ||
| 90 | + <path | ||
| 91 | + android:fillColor="#00000000" | ||
| 92 | + android:pathData="M0,69L108,69" | ||
| 93 | + android:strokeWidth="0.8" | ||
| 94 | + android:strokeColor="#33FFFFFF" /> | ||
| 95 | + <path | ||
| 96 | + android:fillColor="#00000000" | ||
| 97 | + android:pathData="M0,79L108,79" | ||
| 98 | + android:strokeWidth="0.8" | ||
| 99 | + android:strokeColor="#33FFFFFF" /> | ||
| 100 | + <path | ||
| 101 | + android:fillColor="#00000000" | ||
| 102 | + android:pathData="M0,89L108,89" | ||
| 103 | + android:strokeWidth="0.8" | ||
| 104 | + android:strokeColor="#33FFFFFF" /> | ||
| 105 | + <path | ||
| 106 | + android:fillColor="#00000000" | ||
| 107 | + android:pathData="M0,99L108,99" | ||
| 108 | + android:strokeWidth="0.8" | ||
| 109 | + android:strokeColor="#33FFFFFF" /> | ||
| 110 | + <path | ||
| 111 | + android:fillColor="#00000000" | ||
| 112 | + android:pathData="M19,29L89,29" | ||
| 113 | + android:strokeWidth="0.8" | ||
| 114 | + android:strokeColor="#33FFFFFF" /> | ||
| 115 | + <path | ||
| 116 | + android:fillColor="#00000000" | ||
| 117 | + android:pathData="M19,39L89,39" | ||
| 118 | + android:strokeWidth="0.8" | ||
| 119 | + android:strokeColor="#33FFFFFF" /> | ||
| 120 | + <path | ||
| 121 | + android:fillColor="#00000000" | ||
| 122 | + android:pathData="M19,49L89,49" | ||
| 123 | + android:strokeWidth="0.8" | ||
| 124 | + android:strokeColor="#33FFFFFF" /> | ||
| 125 | + <path | ||
| 126 | + android:fillColor="#00000000" | ||
| 127 | + android:pathData="M19,59L89,59" | ||
| 128 | + android:strokeWidth="0.8" | ||
| 129 | + android:strokeColor="#33FFFFFF" /> | ||
| 130 | + <path | ||
| 131 | + android:fillColor="#00000000" | ||
| 132 | + android:pathData="M19,69L89,69" | ||
| 133 | + android:strokeWidth="0.8" | ||
| 134 | + android:strokeColor="#33FFFFFF" /> | ||
| 135 | + <path | ||
| 136 | + android:fillColor="#00000000" | ||
| 137 | + android:pathData="M19,79L89,79" | ||
| 138 | + android:strokeWidth="0.8" | ||
| 139 | + android:strokeColor="#33FFFFFF" /> | ||
| 140 | + <path | ||
| 141 | + android:fillColor="#00000000" | ||
| 142 | + android:pathData="M29,19L29,89" | ||
| 143 | + android:strokeWidth="0.8" | ||
| 144 | + android:strokeColor="#33FFFFFF" /> | ||
| 145 | + <path | ||
| 146 | + android:fillColor="#00000000" | ||
| 147 | + android:pathData="M39,19L39,89" | ||
| 148 | + android:strokeWidth="0.8" | ||
| 149 | + android:strokeColor="#33FFFFFF" /> | ||
| 150 | + <path | ||
| 151 | + android:fillColor="#00000000" | ||
| 152 | + android:pathData="M49,19L49,89" | ||
| 153 | + android:strokeWidth="0.8" | ||
| 154 | + android:strokeColor="#33FFFFFF" /> | ||
| 155 | + <path | ||
| 156 | + android:fillColor="#00000000" | ||
| 157 | + android:pathData="M59,19L59,89" | ||
| 158 | + android:strokeWidth="0.8" | ||
| 159 | + android:strokeColor="#33FFFFFF" /> | ||
| 160 | + <path | ||
| 161 | + android:fillColor="#00000000" | ||
| 162 | + android:pathData="M69,19L69,89" | ||
| 163 | + android:strokeWidth="0.8" | ||
| 164 | + android:strokeColor="#33FFFFFF" /> | ||
| 165 | + <path | ||
| 166 | + android:fillColor="#00000000" | ||
| 167 | + android:pathData="M79,19L79,89" | ||
| 168 | + android:strokeWidth="0.8" | ||
| 169 | + android:strokeColor="#33FFFFFF" /> | ||
| 170 | +</vector> |
| 1 | +<?xml version="1.0" encoding="utf-8"?> | ||
| 2 | +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 3 | + xmlns:tools="http://schemas.android.com/tools" | ||
| 4 | + android:layout_width="match_parent" | ||
| 5 | + android:layout_height="match_parent" | ||
| 6 | + tools:context=".MainActivity"> | ||
| 7 | + | ||
| 8 | + <io.livekit.android.renderer.SurfaceViewRenderer | ||
| 9 | + android:id="@+id/renderer" | ||
| 10 | + android:layout_width="match_parent" | ||
| 11 | + android:layout_height="match_parent" /> | ||
| 12 | + | ||
| 13 | + <ProgressBar | ||
| 14 | + android:id="@+id/progress" | ||
| 15 | + android:layout_width="wrap_content" | ||
| 16 | + android:layout_height="wrap_content" | ||
| 17 | + android:layout_gravity="center" /> | ||
| 18 | + | ||
| 19 | +</FrameLayout> |
不能预览此文件类型
不能预览此文件类型
不能预览此文件类型
不能预览此文件类型
不能预览此文件类型
不能预览此文件类型
不能预览此文件类型
不能预览此文件类型
不能预览此文件类型
不能预览此文件类型
| 1 | +<resources xmlns:tools="http://schemas.android.com/tools"> | ||
| 2 | + <!-- Base application theme. --> | ||
| 3 | + <style name="Theme.Livekitandroid" parent="Theme.MaterialComponents.DayNight.DarkActionBar"> | ||
| 4 | + <!-- Primary brand color. --> | ||
| 5 | + <item name="colorPrimary">@color/purple_200</item> | ||
| 6 | + <item name="colorPrimaryVariant">@color/purple_700</item> | ||
| 7 | + <item name="colorOnPrimary">@color/black</item> | ||
| 8 | + <!-- Secondary brand color. --> | ||
| 9 | + <item name="colorSecondary">@color/teal_200</item> | ||
| 10 | + <item name="colorSecondaryVariant">@color/teal_200</item> | ||
| 11 | + <item name="colorOnSecondary">@color/black</item> | ||
| 12 | + <!-- Status bar color. --> | ||
| 13 | + <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item> | ||
| 14 | + <!-- Customize your theme here. --> | ||
| 15 | + </style> | ||
| 16 | +</resources> |
| 1 | +<?xml version="1.0" encoding="utf-8"?> | ||
| 2 | +<resources> | ||
| 3 | + <color name="purple_200">#FFBB86FC</color> | ||
| 4 | + <color name="purple_500">#FF6200EE</color> | ||
| 5 | + <color name="purple_700">#FF3700B3</color> | ||
| 6 | + <color name="teal_200">#FF03DAC5</color> | ||
| 7 | + <color name="teal_700">#FF018786</color> | ||
| 8 | + <color name="black">#FF000000</color> | ||
| 9 | + <color name="white">#FFFFFFFF</color> | ||
| 10 | +</resources> |
| 1 | +<resources xmlns:tools="http://schemas.android.com/tools"> | ||
| 2 | + <!-- Base application theme. --> | ||
| 3 | + <style name="Theme.Livekitandroid" parent="Theme.MaterialComponents.DayNight.DarkActionBar"> | ||
| 4 | + <!-- Primary brand color. --> | ||
| 5 | + <item name="colorPrimary">@color/purple_500</item> | ||
| 6 | + <item name="colorPrimaryVariant">@color/purple_700</item> | ||
| 7 | + <item name="colorOnPrimary">@color/white</item> | ||
| 8 | + <!-- Secondary brand color. --> | ||
| 9 | + <item name="colorSecondary">@color/teal_200</item> | ||
| 10 | + <item name="colorSecondaryVariant">@color/teal_700</item> | ||
| 11 | + <item name="colorOnSecondary">@color/black</item> | ||
| 12 | + <!-- Status bar color. --> | ||
| 13 | + <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item> | ||
| 14 | + <!-- Customize your theme here. --> | ||
| 15 | + </style> | ||
| 16 | +</resources> |
| 1 | +<?xml version="1.0" encoding="utf-8"?> | ||
| 2 | +<network-security-config> | ||
| 3 | + <domain-config cleartextTrafficPermitted="true"> | ||
| 4 | + <domain includeSubdomains="true">example.com</domain> | ||
| 5 | + </domain-config> | ||
| 6 | + | ||
| 7 | + <base-config cleartextTrafficPermitted="true"> | ||
| 8 | + <trust-anchors> | ||
| 9 | + <certificates src="system" /> | ||
| 10 | + </trust-anchors> | ||
| 11 | + </base-config> | ||
| 12 | +</network-security-config> |
| 1 | +package io.livekit.android.sample.basic | ||
| 2 | + | ||
| 3 | +import org.junit.Test | ||
| 4 | + | ||
| 5 | +import org.junit.Assert.* | ||
| 6 | + | ||
| 7 | +/** | ||
| 8 | + * Example local unit test, which will execute on the development machine (host). | ||
| 9 | + * | ||
| 10 | + * See [testing documentation](http://d.android.com/tools/testing). | ||
| 11 | + */ | ||
| 12 | +class ExampleUnitTest { | ||
| 13 | + @Test | ||
| 14 | + fun addition_isCorrect() { | ||
| 15 | + assertEquals(4, 2 + 2) | ||
| 16 | + } | ||
| 17 | +} |
sample-app-common/README.md
0 → 100644
| 1 | <?xml version="1.0" encoding="utf-8"?> | 1 | <?xml version="1.0" encoding="utf-8"?> |
| 2 | <manifest xmlns:android="http://schemas.android.com/apk/res/android" | 2 | <manifest xmlns:android="http://schemas.android.com/apk/res/android" |
| 3 | - package="io.livekit.android.sample"> | 3 | + package="io.livekit.android.sample.common"> |
| 4 | 4 | ||
| 5 | <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> | 5 | <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> |
| 6 | <uses-permission android:name="android.permission.INTERNET" /> | 6 | <uses-permission android:name="android.permission.INTERNET" /> |
| @@ -2,6 +2,7 @@ package io.livekit.android.sample | @@ -2,6 +2,7 @@ package io.livekit.android.sample | ||
| 2 | 2 | ||
| 3 | import android.app.Application | 3 | import android.app.Application |
| 4 | import android.content.Intent | 4 | import android.content.Intent |
| 5 | +import android.media.projection.MediaProjectionManager | ||
| 5 | import androidx.lifecycle.AndroidViewModel | 6 | import androidx.lifecycle.AndroidViewModel |
| 6 | import androidx.lifecycle.LiveData | 7 | import androidx.lifecycle.LiveData |
| 7 | import androidx.lifecycle.MutableLiveData | 8 | import androidx.lifecycle.MutableLiveData |
| @@ -57,6 +58,7 @@ class CallViewModel( | @@ -57,6 +58,7 @@ class CallViewModel( | ||
| 57 | 58 | ||
| 58 | private var localScreencastTrack: LocalScreencastVideoTrack? = null | 59 | private var localScreencastTrack: LocalScreencastVideoTrack? = null |
| 59 | 60 | ||
| 61 | + // Controls | ||
| 60 | private val mutableMicEnabled = MutableLiveData(true) | 62 | private val mutableMicEnabled = MutableLiveData(true) |
| 61 | val micEnabled = mutableMicEnabled.hide() | 63 | val micEnabled = mutableMicEnabled.hide() |
| 62 | 64 | ||
| @@ -69,18 +71,22 @@ class CallViewModel( | @@ -69,18 +71,22 @@ class CallViewModel( | ||
| 69 | private val mutableScreencastEnabled = MutableLiveData(false) | 71 | private val mutableScreencastEnabled = MutableLiveData(false) |
| 70 | val screenshareEnabled = mutableScreencastEnabled.hide() | 72 | val screenshareEnabled = mutableScreencastEnabled.hide() |
| 71 | 73 | ||
| 74 | + // Emits a string whenever a data message is received. | ||
| 72 | private val mutableDataReceived = MutableSharedFlow<String>() | 75 | private val mutableDataReceived = MutableSharedFlow<String>() |
| 73 | val dataReceived = mutableDataReceived | 76 | val dataReceived = mutableDataReceived |
| 74 | 77 | ||
| 78 | + // Whether other participants are allowed to subscribe to this participant's tracks. | ||
| 75 | private val mutablePermissionAllowed = MutableStateFlow(true) | 79 | private val mutablePermissionAllowed = MutableStateFlow(true) |
| 76 | val permissionAllowed = mutablePermissionAllowed.hide() | 80 | val permissionAllowed = mutablePermissionAllowed.hide() |
| 77 | 81 | ||
| 78 | init { | 82 | init { |
| 79 | viewModelScope.launch { | 83 | viewModelScope.launch { |
| 84 | + // Collect any errors. | ||
| 80 | launch { | 85 | launch { |
| 81 | error.collect { Timber.e(it) } | 86 | error.collect { Timber.e(it) } |
| 82 | } | 87 | } |
| 83 | 88 | ||
| 89 | + // Handle any changes in speakers. | ||
| 84 | launch { | 90 | launch { |
| 85 | combine(participants, activeSpeakers) { participants, speakers -> participants to speakers } | 91 | combine(participants, activeSpeakers) { participants, speakers -> participants to speakers } |
| 86 | .collect { (participantsList, speakers) -> | 92 | .collect { (participantsList, speakers) -> |
| @@ -93,6 +99,7 @@ class CallViewModel( | @@ -93,6 +99,7 @@ class CallViewModel( | ||
| 93 | } | 99 | } |
| 94 | 100 | ||
| 95 | launch { | 101 | launch { |
| 102 | + // Handle room events. | ||
| 96 | room.events.collect { | 103 | room.events.collect { |
| 97 | when (it) { | 104 | when (it) { |
| 98 | is RoomEvent.FailedToConnect -> mutableError.value = it.error | 105 | is RoomEvent.FailedToConnect -> mutableError.value = it.error |
| @@ -124,6 +131,7 @@ class CallViewModel( | @@ -124,6 +131,7 @@ class CallViewModel( | ||
| 124 | localParticipant.setCameraEnabled(true) | 131 | localParticipant.setCameraEnabled(true) |
| 125 | mutableCameraEnabled.postValue(localParticipant.isCameraEnabled()) | 132 | mutableCameraEnabled.postValue(localParticipant.isCameraEnabled()) |
| 126 | 133 | ||
| 134 | + // Update the speaker | ||
| 127 | handlePrimarySpeaker(emptyList(), emptyList(), room) | 135 | handlePrimarySpeaker(emptyList(), emptyList(), room) |
| 128 | } catch (e: Throwable) { | 136 | } catch (e: Throwable) { |
| 129 | mutableError.value = e | 137 | mutableError.value = e |
| @@ -167,6 +175,10 @@ class CallViewModel( | @@ -167,6 +175,10 @@ class CallViewModel( | ||
| 167 | mutablePrimarySpeaker.value = speaker | 175 | mutablePrimarySpeaker.value = speaker |
| 168 | } | 176 | } |
| 169 | 177 | ||
| 178 | + /** | ||
| 179 | + * Start a screen capture with the result intent from | ||
| 180 | + * [MediaProjectionManager.createScreenCaptureIntent] | ||
| 181 | + */ | ||
| 170 | fun startScreenCapture(mediaProjectionPermissionResultData: Intent) { | 182 | fun startScreenCapture(mediaProjectionPermissionResultData: Intent) { |
| 171 | val localParticipant = room.localParticipant | 183 | val localParticipant = room.localParticipant |
| 172 | viewModelScope.launch { | 184 | viewModelScope.launch { |
| @@ -243,6 +255,7 @@ class CallViewModel( | @@ -243,6 +255,7 @@ class CallViewModel( | ||
| 243 | room.localParticipant.setTrackSubscriptionPermissions(mutablePermissionAllowed.value) | 255 | room.localParticipant.setTrackSubscriptionPermissions(mutablePermissionAllowed.value) |
| 244 | } | 256 | } |
| 245 | 257 | ||
| 258 | + // Debug functions | ||
| 246 | fun simulateMigration() { | 259 | fun simulateMigration() { |
| 247 | room.sendSimulateScenario( | 260 | room.sendSimulateScenario( |
| 248 | LivekitRtc.SimulateScenario.newBuilder() | 261 | LivekitRtc.SimulateScenario.newBuilder() |
| @@ -4,6 +4,7 @@ import android.app.Application | @@ -4,6 +4,7 @@ import android.app.Application | ||
| 4 | import androidx.core.content.edit | 4 | import androidx.core.content.edit |
| 5 | import androidx.lifecycle.AndroidViewModel | 5 | import androidx.lifecycle.AndroidViewModel |
| 6 | import androidx.preference.PreferenceManager | 6 | import androidx.preference.PreferenceManager |
| 7 | +import io.livekit.android.sample.common.BuildConfig | ||
| 7 | 8 | ||
| 8 | class MainViewModel(application: Application) : AndroidViewModel(application) { | 9 | class MainViewModel(application: Application) : AndroidViewModel(application) { |
| 9 | 10 |
| @@ -13,6 +13,7 @@ fun ComponentActivity.requestNeededPermissions() { | @@ -13,6 +13,7 @@ fun ComponentActivity.requestNeededPermissions() { | ||
| 13 | registerForActivityResult( | 13 | registerForActivityResult( |
| 14 | ActivityResultContracts.RequestMultiplePermissions() | 14 | ActivityResultContracts.RequestMultiplePermissions() |
| 15 | ) { grants -> | 15 | ) { grants -> |
| 16 | + // Check if any permissions weren't granted. | ||
| 16 | for (grant in grants.entries) { | 17 | for (grant in grants.entries) { |
| 17 | if (!grant.value) { | 18 | if (!grant.value) { |
| 18 | Toast.makeText( | 19 | Toast.makeText( |
| @@ -24,6 +25,7 @@ fun ComponentActivity.requestNeededPermissions() { | @@ -24,6 +25,7 @@ fun ComponentActivity.requestNeededPermissions() { | ||
| 24 | } | 25 | } |
| 25 | } | 26 | } |
| 26 | } | 27 | } |
| 28 | + | ||
| 27 | val neededPermissions = listOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA) | 29 | val neededPermissions = listOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA) |
| 28 | .let { perms -> | 30 | .let { perms -> |
| 29 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { | 31 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { |
| @@ -32,12 +34,7 @@ fun ComponentActivity.requestNeededPermissions() { | @@ -32,12 +34,7 @@ fun ComponentActivity.requestNeededPermissions() { | ||
| 32 | perms | 34 | perms |
| 33 | } | 35 | } |
| 34 | } | 36 | } |
| 35 | - .filter { | ||
| 36 | - ContextCompat.checkSelfPermission( | ||
| 37 | - this, | ||
| 38 | - it | ||
| 39 | - ) == PackageManager.PERMISSION_DENIED | ||
| 40 | - } | 37 | + .filter { ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_DENIED } |
| 41 | .toTypedArray() | 38 | .toTypedArray() |
| 42 | 39 | ||
| 43 | if (neededPermissions.isNotEmpty()) { | 40 | if (neededPermissions.isNotEmpty()) { |
sample-app-compose/README.md
0 → 100644
| 1 | plugins { | 1 | plugins { |
| 2 | id 'com.android.application' | 2 | id 'com.android.application' |
| 3 | - id 'kotlin-android' | 3 | + id 'org.jetbrains.kotlin.android' |
| 4 | id 'kotlin-parcelize' | 4 | id 'kotlin-parcelize' |
| 5 | } | 5 | } |
| 6 | 6 | ||
| @@ -32,7 +32,7 @@ android { | @@ -32,7 +32,7 @@ android { | ||
| 32 | } | 32 | } |
| 33 | kotlinOptions { | 33 | kotlinOptions { |
| 34 | jvmTarget = java_version | 34 | jvmTarget = java_version |
| 35 | - freeCompilerArgs += '-Xopt-in=kotlin.RequiresOptIn' | 35 | + freeCompilerArgs += '-opt-in=kotlin.RequiresOptIn' |
| 36 | } | 36 | } |
| 37 | buildFeatures { | 37 | buildFeatures { |
| 38 | compose true | 38 | compose true |
| @@ -13,7 +13,6 @@ import androidx.compose.foundation.background | @@ -13,7 +13,6 @@ import androidx.compose.foundation.background | ||
| 13 | import androidx.compose.foundation.layout.* | 13 | import androidx.compose.foundation.layout.* |
| 14 | import androidx.compose.foundation.lazy.LazyRow | 14 | import androidx.compose.foundation.lazy.LazyRow |
| 15 | import androidx.compose.material.* | 15 | import androidx.compose.material.* |
| 16 | -import androidx.compose.material.ripple.rememberRipple | ||
| 17 | import androidx.compose.runtime.* | 16 | import androidx.compose.runtime.* |
| 18 | import androidx.compose.runtime.livedata.observeAsState | 17 | import androidx.compose.runtime.livedata.observeAsState |
| 19 | import androidx.compose.ui.Alignment | 18 | import androidx.compose.ui.Alignment |
| @@ -217,7 +216,6 @@ class CallActivity : AppCompatActivity() { | @@ -217,7 +216,6 @@ class CallActivity : AppCompatActivity() { | ||
| 217 | ) { | 216 | ) { |
| 218 | Surface( | 217 | Surface( |
| 219 | onClick = { viewModel.setMicEnabled(!micEnabled) }, | 218 | onClick = { viewModel.setMicEnabled(!micEnabled) }, |
| 220 | - indication = rememberRipple(false), | ||
| 221 | modifier = Modifier | 219 | modifier = Modifier |
| 222 | .size(controlSize) | 220 | .size(controlSize) |
| 223 | .padding(controlPadding) | 221 | .padding(controlPadding) |
| @@ -232,7 +230,6 @@ class CallActivity : AppCompatActivity() { | @@ -232,7 +230,6 @@ class CallActivity : AppCompatActivity() { | ||
| 232 | } | 230 | } |
| 233 | Surface( | 231 | Surface( |
| 234 | onClick = { viewModel.setCameraEnabled(!videoEnabled) }, | 232 | onClick = { viewModel.setCameraEnabled(!videoEnabled) }, |
| 235 | - indication = rememberRipple(false), | ||
| 236 | modifier = Modifier | 233 | modifier = Modifier |
| 237 | .size(controlSize) | 234 | .size(controlSize) |
| 238 | .padding(controlPadding) | 235 | .padding(controlPadding) |
| @@ -247,7 +244,6 @@ class CallActivity : AppCompatActivity() { | @@ -247,7 +244,6 @@ class CallActivity : AppCompatActivity() { | ||
| 247 | } | 244 | } |
| 248 | Surface( | 245 | Surface( |
| 249 | onClick = { viewModel.flipCamera() }, | 246 | onClick = { viewModel.flipCamera() }, |
| 250 | - indication = rememberRipple(false), | ||
| 251 | modifier = Modifier | 247 | modifier = Modifier |
| 252 | .size(controlSize) | 248 | .size(controlSize) |
| 253 | .padding(controlPadding) | 249 | .padding(controlPadding) |
| @@ -266,7 +262,6 @@ class CallActivity : AppCompatActivity() { | @@ -266,7 +262,6 @@ class CallActivity : AppCompatActivity() { | ||
| 266 | viewModel.stopScreenCapture() | 262 | viewModel.stopScreenCapture() |
| 267 | } | 263 | } |
| 268 | }, | 264 | }, |
| 269 | - indication = rememberRipple(false), | ||
| 270 | modifier = Modifier | 265 | modifier = Modifier |
| 271 | .size(controlSize) | 266 | .size(controlSize) |
| 272 | .padding(controlPadding) | 267 | .padding(controlPadding) |
| @@ -284,7 +279,6 @@ class CallActivity : AppCompatActivity() { | @@ -284,7 +279,6 @@ class CallActivity : AppCompatActivity() { | ||
| 284 | var messageToSend by remember { mutableStateOf("") } | 279 | var messageToSend by remember { mutableStateOf("") } |
| 285 | Surface( | 280 | Surface( |
| 286 | onClick = { showMessageDialog = true }, | 281 | onClick = { showMessageDialog = true }, |
| 287 | - indication = rememberRipple(false), | ||
| 288 | modifier = Modifier | 282 | modifier = Modifier |
| 289 | .size(controlSize) | 283 | .size(controlSize) |
| 290 | .padding(controlPadding) | 284 | .padding(controlPadding) |
| @@ -335,7 +329,6 @@ class CallActivity : AppCompatActivity() { | @@ -335,7 +329,6 @@ class CallActivity : AppCompatActivity() { | ||
| 335 | } | 329 | } |
| 336 | Surface( | 330 | Surface( |
| 337 | onClick = { onExitClick() }, | 331 | onClick = { onExitClick() }, |
| 338 | - indication = rememberRipple(false), | ||
| 339 | modifier = Modifier | 332 | modifier = Modifier |
| 340 | .size(controlSize) | 333 | .size(controlSize) |
| 341 | .padding(controlPadding) | 334 | .padding(controlPadding) |
| @@ -358,7 +351,6 @@ class CallActivity : AppCompatActivity() { | @@ -358,7 +351,6 @@ class CallActivity : AppCompatActivity() { | ||
| 358 | var showAudioDeviceDialog by remember { mutableStateOf(false) } | 351 | var showAudioDeviceDialog by remember { mutableStateOf(false) } |
| 359 | Surface( | 352 | Surface( |
| 360 | onClick = { showAudioDeviceDialog = true }, | 353 | onClick = { showAudioDeviceDialog = true }, |
| 361 | - indication = rememberRipple(false), | ||
| 362 | modifier = Modifier | 354 | modifier = Modifier |
| 363 | .size(controlSize) | 355 | .size(controlSize) |
| 364 | .padding(controlPadding) | 356 | .padding(controlPadding) |
| @@ -380,7 +372,6 @@ class CallActivity : AppCompatActivity() { | @@ -380,7 +372,6 @@ class CallActivity : AppCompatActivity() { | ||
| 380 | } | 372 | } |
| 381 | Surface( | 373 | Surface( |
| 382 | onClick = { viewModel.toggleSubscriptionPermissions() }, | 374 | onClick = { viewModel.toggleSubscriptionPermissions() }, |
| 383 | - indication = rememberRipple(false), | ||
| 384 | modifier = Modifier | 375 | modifier = Modifier |
| 385 | .size(controlSize) | 376 | .size(controlSize) |
| 386 | .padding(controlPadding) | 377 | .padding(controlPadding) |
| @@ -397,7 +388,6 @@ class CallActivity : AppCompatActivity() { | @@ -397,7 +388,6 @@ class CallActivity : AppCompatActivity() { | ||
| 397 | var showDebugDialog by remember { mutableStateOf(false) } | 388 | var showDebugDialog by remember { mutableStateOf(false) } |
| 398 | Surface( | 389 | Surface( |
| 399 | onClick = { showDebugDialog = true }, | 390 | onClick = { showDebugDialog = true }, |
| 400 | - indication = rememberRipple(false), | ||
| 401 | modifier = Modifier | 391 | modifier = Modifier |
| 402 | .size(controlSize) | 392 | .size(controlSize) |
| 403 | .padding(controlPadding) | 393 | .padding(controlPadding) |
sample-app/README.md
0 → 100644
| 1 | -apply plugin: 'com.android.application' | ||
| 2 | - | ||
| 3 | -apply plugin: 'kotlin-android' | ||
| 4 | -apply plugin: 'kotlin-parcelize' | 1 | +plugins { |
| 2 | + id 'com.android.application' | ||
| 3 | + id 'org.jetbrains.kotlin.android' | ||
| 4 | + id 'kotlin-parcelize' | ||
| 5 | +} | ||
| 5 | 6 | ||
| 6 | android { | 7 | android { |
| 7 | compileSdkVersion androidSdk.compileVersion | 8 | compileSdkVersion androidSdk.compileVersion |
| @@ -36,16 +37,16 @@ dependencies { | @@ -36,16 +37,16 @@ dependencies { | ||
| 36 | implementation fileTree(dir: 'libs', include: ['*.jar']) | 37 | implementation fileTree(dir: 'libs', include: ['*.jar']) |
| 37 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" | 38 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" |
| 38 | implementation deps.coroutines.lib | 39 | implementation deps.coroutines.lib |
| 39 | - implementation 'com.google.android.material:material:1.4.0' | ||
| 40 | - implementation 'androidx.appcompat:appcompat:1.4.0' | 40 | + implementation 'com.google.android.material:material:1.6.1' |
| 41 | + implementation 'androidx.appcompat:appcompat:1.4.2' | ||
| 41 | implementation "androidx.core:core-ktx:${versions.androidx_core}" | 42 | implementation "androidx.core:core-ktx:${versions.androidx_core}" |
| 42 | - implementation "androidx.activity:activity-ktx:1.4.0" | ||
| 43 | - implementation 'androidx.fragment:fragment-ktx:1.3.6' | 43 | + implementation "androidx.activity:activity-ktx:1.5.1" |
| 44 | + implementation 'androidx.fragment:fragment-ktx:1.5.1' | ||
| 44 | implementation "androidx.viewpager2:viewpager2:1.0.0" | 45 | implementation "androidx.viewpager2:viewpager2:1.0.0" |
| 45 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:${versions.androidx_lifecycle}" | 46 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:${versions.androidx_lifecycle}" |
| 46 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${versions.androidx_lifecycle}" | 47 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${versions.androidx_lifecycle}" |
| 47 | implementation "androidx.lifecycle:lifecycle-common-java8:${versions.androidx_lifecycle}" | 48 | implementation "androidx.lifecycle:lifecycle-common-java8:${versions.androidx_lifecycle}" |
| 48 | - implementation 'com.google.android.material:material:1.3.0' | 49 | + implementation 'com.google.android.material:material:1.6.1' |
| 49 | implementation "com.github.lisawray.groupie:groupie:${versions.groupie}" | 50 | implementation "com.github.lisawray.groupie:groupie:${versions.groupie}" |
| 50 | implementation "com.github.lisawray.groupie:groupie-viewbinding:${versions.groupie}" | 51 | implementation "com.github.lisawray.groupie:groupie-viewbinding:${versions.groupie}" |
| 51 | implementation deps.timber | 52 | implementation deps.timber |
| @@ -8,3 +8,4 @@ rootProject.name='livekit-android' | @@ -8,3 +8,4 @@ rootProject.name='livekit-android' | ||
| 8 | include ':sample-app-common' | 8 | include ':sample-app-common' |
| 9 | include ':livekit-lint' | 9 | include ':livekit-lint' |
| 10 | include ':video-encode-decode-test' | 10 | include ':video-encode-decode-test' |
| 11 | +include ':sample-app-basic' |
video-encode-decode-test/README.md
0 → 100644
| 1 | plugins { | 1 | plugins { |
| 2 | id 'com.android.application' | 2 | id 'com.android.application' |
| 3 | - id 'kotlin-android' | 3 | + id 'org.jetbrains.kotlin.android' |
| 4 | id 'kotlin-parcelize' | 4 | id 'kotlin-parcelize' |
| 5 | } | 5 | } |
| 6 | 6 | ||
| @@ -57,7 +57,7 @@ android { | @@ -57,7 +57,7 @@ android { | ||
| 57 | } | 57 | } |
| 58 | kotlinOptions { | 58 | kotlinOptions { |
| 59 | jvmTarget = java_version | 59 | jvmTarget = java_version |
| 60 | - freeCompilerArgs += '-Xopt-in=kotlin.RequiresOptIn' | 60 | + freeCompilerArgs += '-opt-in=kotlin.RequiresOptIn' |
| 61 | } | 61 | } |
| 62 | buildFeatures { | 62 | buildFeatures { |
| 63 | compose true | 63 | compose true |
| @@ -17,11 +17,11 @@ import androidx.constraintlayout.compose.ConstraintLayout | @@ -17,11 +17,11 @@ import androidx.constraintlayout.compose.ConstraintLayout | ||
| 17 | import androidx.constraintlayout.compose.Dimension | 17 | import androidx.constraintlayout.compose.Dimension |
| 18 | import androidx.lifecycle.ViewModel | 18 | import androidx.lifecycle.ViewModel |
| 19 | import androidx.lifecycle.ViewModelProvider | 19 | import androidx.lifecycle.ViewModelProvider |
| 20 | +import androidx.lifecycle.viewmodel.CreationExtras | ||
| 20 | import com.google.accompanist.pager.ExperimentalPagerApi | 21 | import com.google.accompanist.pager.ExperimentalPagerApi |
| 21 | import io.livekit.android.composesample.ui.theme.AppTheme | 22 | import io.livekit.android.composesample.ui.theme.AppTheme |
| 22 | import kotlinx.parcelize.Parcelize | 23 | import kotlinx.parcelize.Parcelize |
| 23 | 24 | ||
| 24 | -@OptIn(ExperimentalPagerApi::class) | ||
| 25 | class CallActivity : AppCompatActivity() { | 25 | class CallActivity : AppCompatActivity() { |
| 26 | 26 | ||
| 27 | private lateinit var viewModel1: CallViewModel | 27 | private lateinit var viewModel1: CallViewModel |
| @@ -31,11 +31,13 @@ class CallActivity : AppCompatActivity() { | @@ -31,11 +31,13 @@ class CallActivity : AppCompatActivity() { | ||
| 31 | override fun onCreate(savedInstanceState: Bundle?) { | 31 | override fun onCreate(savedInstanceState: Bundle?) { |
| 32 | super.onCreate(savedInstanceState) | 32 | super.onCreate(savedInstanceState) |
| 33 | 33 | ||
| 34 | - val viewModelProvider = ViewModelProvider(this, object : ViewModelProvider.KeyedFactory() { | ||
| 35 | - override fun <T : ViewModel> create(key: String, modelClass: Class<T>): T { | 34 | + val viewModelProvider = ViewModelProvider(this, object : ViewModelProvider.Factory { |
| 35 | + override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T { | ||
| 36 | + | ||
| 36 | val args = intent.getParcelableExtra<BundleArgs>(KEY_ARGS) | 37 | val args = intent.getParcelableExtra<BundleArgs>(KEY_ARGS) |
| 37 | ?: throw NullPointerException("args is null!") | 38 | ?: throw NullPointerException("args is null!") |
| 38 | 39 | ||
| 40 | + val key = extras[ViewModelProvider.NewInstanceFactory.VIEW_MODEL_KEY] | ||
| 39 | val token = if (key == VIEWMODEL_KEY1) args.token1 else args.token2 | 41 | val token = if (key == VIEWMODEL_KEY1) args.token1 else args.token2 |
| 40 | val showVideo = key == VIEWMODEL_KEY1 | 42 | val showVideo = key == VIEWMODEL_KEY1 |
| 41 | @Suppress("UNCHECKED_CAST") | 43 | @Suppress("UNCHECKED_CAST") |
-
请 注册 或 登录 后发表评论