Committed by
GitHub
Fix Subscriber primary data channel (#33)
* Properly receive data channels through subscriber * clean up log * compose sample app data sending and receiving * sample app send message example
正在显示
10 个修改的文件
包含
187 行增加
和
58 行删除
| 1 | <?xml version="1.0" encoding="UTF-8"?> | 1 | <?xml version="1.0" encoding="UTF-8"?> |
| 2 | <project version="4"> | 2 | <project version="4"> |
| 3 | <component name="CompilerConfiguration"> | 3 | <component name="CompilerConfiguration"> |
| 4 | - <bytecodeTargetLevel target="1.8"> | ||
| 5 | - <module name="livekit-android.livekit-android-sdk" target="11" /> | ||
| 6 | - <module name="livekit-android.sample-app" target="11" /> | ||
| 7 | - <module name="livekit-android.sample-app-common" target="11" /> | ||
| 8 | - <module name="livekit-android.sample-app-compose" target="11" /> | ||
| 9 | - </bytecodeTargetLevel> | 4 | + <bytecodeTargetLevel target="1.8" /> |
| 10 | </component> | 5 | </component> |
| 11 | </project> | 6 | </project> |
| @@ -12,7 +12,6 @@ class PublisherTransportObserver( | @@ -12,7 +12,6 @@ class PublisherTransportObserver( | ||
| 12 | private val client: SignalClient, | 12 | private val client: SignalClient, |
| 13 | ) : PeerConnection.Observer, PeerConnectionTransport.Listener { | 13 | ) : PeerConnection.Observer, PeerConnectionTransport.Listener { |
| 14 | 14 | ||
| 15 | - var dataChannelListener: ((DataChannel?) -> Unit)? = null | ||
| 16 | var iceConnectionChangeListener: ((newState: PeerConnection.IceConnectionState?) -> Unit)? = | 15 | var iceConnectionChangeListener: ((newState: PeerConnection.IceConnectionState?) -> Unit)? = |
| 17 | null | 16 | null |
| 18 | 17 | ||
| @@ -63,7 +62,6 @@ class PublisherTransportObserver( | @@ -63,7 +62,6 @@ class PublisherTransportObserver( | ||
| 63 | } | 62 | } |
| 64 | 63 | ||
| 65 | override fun onDataChannel(dataChannel: DataChannel?) { | 64 | override fun onDataChannel(dataChannel: DataChannel?) { |
| 66 | - dataChannelListener?.invoke(dataChannel) | ||
| 67 | } | 65 | } |
| 68 | 66 | ||
| 69 | override fun onTrack(transceiver: RtpTransceiver?) { | 67 | override fun onTrack(transceiver: RtpTransceiver?) { |
| @@ -179,10 +179,7 @@ internal constructor( | @@ -179,10 +179,7 @@ internal constructor( | ||
| 179 | 179 | ||
| 180 | if (joinResponse.subscriberPrimary) { | 180 | if (joinResponse.subscriberPrimary) { |
| 181 | // in subscriber primary mode, server side opens sub data channels. | 181 | // in subscriber primary mode, server side opens sub data channels. |
| 182 | - publisherObserver.dataChannelListener = onDataChannel@{ dataChannel: DataChannel? -> | ||
| 183 | - if (dataChannel == null) { | ||
| 184 | - return@onDataChannel | ||
| 185 | - } | 182 | + subscriberObserver.dataChannelListener = onDataChannel@{ dataChannel: DataChannel -> |
| 186 | when (dataChannel.label()) { | 183 | when (dataChannel.label()) { |
| 187 | RELIABLE_DATA_CHANNEL_LABEL -> reliableDataChannelSub = dataChannel | 184 | RELIABLE_DATA_CHANNEL_LABEL -> reliableDataChannelSub = dataChannel |
| 188 | LOSSY_DATA_CHANNEL_LABEL -> lossyDataChannelSub = dataChannel | 185 | LOSSY_DATA_CHANNEL_LABEL -> lossyDataChannelSub = dataChannel |
| @@ -190,9 +187,9 @@ internal constructor( | @@ -190,9 +187,9 @@ internal constructor( | ||
| 190 | } | 187 | } |
| 191 | dataChannel.registerObserver(this) | 188 | dataChannel.registerObserver(this) |
| 192 | } | 189 | } |
| 193 | - publisherObserver.iceConnectionChangeListener = iceConnectionStateListener | ||
| 194 | - } else { | ||
| 195 | subscriberObserver.iceConnectionChangeListener = iceConnectionStateListener | 190 | subscriberObserver.iceConnectionChangeListener = iceConnectionStateListener |
| 191 | + } else { | ||
| 192 | + publisherObserver.iceConnectionChangeListener = iceConnectionStateListener | ||
| 196 | } | 193 | } |
| 197 | 194 | ||
| 198 | // data channels | 195 | // data channels |
| @@ -282,7 +279,7 @@ internal constructor( | @@ -282,7 +279,7 @@ internal constructor( | ||
| 282 | if (hasPublished) { | 279 | if (hasPublished) { |
| 283 | publisher.negotiate( | 280 | publisher.negotiate( |
| 284 | getPublisherOfferConstraints().apply { | 281 | getPublisherOfferConstraints().apply { |
| 285 | - with(mandatory){ | 282 | + with(mandatory) { |
| 286 | add( | 283 | add( |
| 287 | MediaConstraints.KeyValuePair( | 284 | MediaConstraints.KeyValuePair( |
| 288 | MediaConstraintKeys.ICE_RESTART, | 285 | MediaConstraintKeys.ICE_RESTART, |
| @@ -323,7 +320,7 @@ internal constructor( | @@ -323,7 +320,7 @@ internal constructor( | ||
| 323 | channel.send(buf) | 320 | channel.send(buf) |
| 324 | } | 321 | } |
| 325 | 322 | ||
| 326 | - private suspend fun ensurePublisherConnected(){ | 323 | + private suspend fun ensurePublisherConnected() { |
| 327 | if (!isSubscriberPrimary) { | 324 | if (!isSubscriberPrimary) { |
| 328 | return | 325 | return |
| 329 | } | 326 | } |
| @@ -557,7 +554,7 @@ internal constructor( | @@ -557,7 +554,7 @@ internal constructor( | ||
| 557 | } | 554 | } |
| 558 | } | 555 | } |
| 559 | 556 | ||
| 560 | - internal enum class IceState { | 557 | +internal enum class IceState { |
| 561 | DISCONNECTED, | 558 | DISCONNECTED, |
| 562 | RECONNECTING, | 559 | RECONNECTING, |
| 563 | CONNECTED, | 560 | CONNECTED, |
| @@ -12,6 +12,7 @@ class SubscriberTransportObserver( | @@ -12,6 +12,7 @@ class SubscriberTransportObserver( | ||
| 12 | private val client: SignalClient, | 12 | private val client: SignalClient, |
| 13 | ) : PeerConnection.Observer { | 13 | ) : PeerConnection.Observer { |
| 14 | 14 | ||
| 15 | + var dataChannelListener: ((DataChannel) -> Unit)? = null | ||
| 15 | var iceConnectionChangeListener: ((PeerConnection.IceConnectionState?) -> Unit)? = null | 16 | var iceConnectionChangeListener: ((PeerConnection.IceConnectionState?) -> Unit)? = null |
| 16 | 17 | ||
| 17 | override fun onIceCandidate(candidate: IceCandidate) { | 18 | override fun onIceCandidate(candidate: IceCandidate) { |
| @@ -34,7 +35,7 @@ class SubscriberTransportObserver( | @@ -34,7 +35,7 @@ class SubscriberTransportObserver( | ||
| 34 | } | 35 | } |
| 35 | 36 | ||
| 36 | override fun onDataChannel(channel: DataChannel) { | 37 | override fun onDataChannel(channel: DataChannel) { |
| 37 | - LKLog.v { "onDataChannel" } | 38 | + dataChannelListener?.invoke(channel) |
| 38 | } | 39 | } |
| 39 | 40 | ||
| 40 | override fun onStandardizedIceConnectionChange(newState: PeerConnection.IceConnectionState?) { | 41 | override fun onStandardizedIceConnectionChange(newState: PeerConnection.IceConnectionState?) { |
| @@ -311,7 +311,7 @@ internal constructor( | @@ -311,7 +311,7 @@ internal constructor( | ||
| 311 | } | 311 | } |
| 312 | 312 | ||
| 313 | fun addEncoding(videoEncoding: VideoEncoding, scale: Double) { | 313 | fun addEncoding(videoEncoding: VideoEncoding, scale: Double) { |
| 314 | - if(encodings.size >= VIDEO_RIDS.size) { | 314 | + if (encodings.size >= VIDEO_RIDS.size) { |
| 315 | throw IllegalStateException("Attempting to add more encodings than we have rids for!") | 315 | throw IllegalStateException("Attempting to add more encodings than we have rids for!") |
| 316 | } | 316 | } |
| 317 | val rid = VIDEO_RIDS[encodings.size] | 317 | val rid = VIDEO_RIDS[encodings.size] |
| @@ -348,7 +348,7 @@ internal constructor( | @@ -348,7 +348,7 @@ internal constructor( | ||
| 348 | // presets assume width is longest size | 348 | // presets assume width is longest size |
| 349 | val longestSize = max(width, height) | 349 | val longestSize = max(width, height) |
| 350 | val preset = presets | 350 | val preset = presets |
| 351 | - .firstOrNull { it.capture.width >= longestSize} | 351 | + .firstOrNull { it.capture.width >= longestSize } |
| 352 | ?: presets.last() | 352 | ?: presets.last() |
| 353 | 353 | ||
| 354 | return preset.encoding | 354 | return preset.encoding |
| @@ -435,7 +435,11 @@ internal constructor( | @@ -435,7 +435,11 @@ internal constructor( | ||
| 435 | * @param destination list of participant SIDs to deliver the payload, null to deliver to everyone | 435 | * @param destination list of participant SIDs to deliver the payload, null to deliver to everyone |
| 436 | */ | 436 | */ |
| 437 | @Suppress("unused") | 437 | @Suppress("unused") |
| 438 | - suspend fun publishData(data: ByteArray, reliability: DataPublishReliability, destination: List<String>?) { | 438 | + suspend fun publishData( |
| 439 | + data: ByteArray, | ||
| 440 | + reliability: DataPublishReliability = DataPublishReliability.RELIABLE, | ||
| 441 | + destination: List<String>? = null | ||
| 442 | + ) { | ||
| 439 | if (data.size > RTCEngine.MAX_DATA_PACKET_SIZE) { | 443 | if (data.size > RTCEngine.MAX_DATA_PACKET_SIZE) { |
| 440 | throw IllegalArgumentException("cannot publish data larger than " + RTCEngine.MAX_DATA_PACKET_SIZE) | 444 | throw IllegalArgumentException("cannot publish data larger than " + RTCEngine.MAX_DATA_PACKET_SIZE) |
| 441 | } | 445 | } |
| @@ -444,16 +448,16 @@ internal constructor( | @@ -444,16 +448,16 @@ internal constructor( | ||
| 444 | DataPublishReliability.RELIABLE -> LivekitModels.DataPacket.Kind.RELIABLE | 448 | DataPublishReliability.RELIABLE -> LivekitModels.DataPacket.Kind.RELIABLE |
| 445 | DataPublishReliability.LOSSY -> LivekitModels.DataPacket.Kind.LOSSY | 449 | DataPublishReliability.LOSSY -> LivekitModels.DataPacket.Kind.LOSSY |
| 446 | } | 450 | } |
| 447 | - val packetBuilder = LivekitModels.UserPacket.newBuilder(). | ||
| 448 | - setPayload(ByteString.copyFrom(data)). | ||
| 449 | - setParticipantSid(sid) | 451 | + val packetBuilder = LivekitModels.UserPacket.newBuilder() |
| 452 | + .setPayload(ByteString.copyFrom(data)) | ||
| 453 | + .setParticipantSid(sid) | ||
| 450 | if (destination != null) { | 454 | if (destination != null) { |
| 451 | packetBuilder.addAllDestinationSids(destination) | 455 | packetBuilder.addAllDestinationSids(destination) |
| 452 | } | 456 | } |
| 453 | - val dataPacket = LivekitModels.DataPacket.newBuilder(). | ||
| 454 | - setUser(packetBuilder). | ||
| 455 | - setKind(kind). | ||
| 456 | - build() | 457 | + val dataPacket = LivekitModels.DataPacket.newBuilder() |
| 458 | + .setUser(packetBuilder) | ||
| 459 | + .setKind(kind) | ||
| 460 | + .build() | ||
| 457 | 461 | ||
| 458 | engine.sendData(dataPacket) | 462 | engine.sendData(dataPacket) |
| 459 | } | 463 | } |
| @@ -17,6 +17,8 @@ import io.livekit.android.util.flow | @@ -17,6 +17,8 @@ import io.livekit.android.util.flow | ||
| 17 | import kotlinx.coroutines.ExperimentalCoroutinesApi | 17 | import kotlinx.coroutines.ExperimentalCoroutinesApi |
| 18 | import kotlinx.coroutines.flow.* | 18 | import kotlinx.coroutines.flow.* |
| 19 | import kotlinx.coroutines.launch | 19 | import kotlinx.coroutines.launch |
| 20 | +import okio.Utf8 | ||
| 21 | +import java.nio.charset.Charset | ||
| 20 | 22 | ||
| 21 | @OptIn(ExperimentalCoroutinesApi::class) | 23 | @OptIn(ExperimentalCoroutinesApi::class) |
| 22 | class CallViewModel( | 24 | class CallViewModel( |
| @@ -69,6 +71,9 @@ class CallViewModel( | @@ -69,6 +71,9 @@ 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 | + private val mutableDataReceived = MutableSharedFlow<String>() | ||
| 75 | + val dataReceived = mutableDataReceived | ||
| 76 | + | ||
| 72 | init { | 77 | init { |
| 73 | viewModelScope.launch { | 78 | viewModelScope.launch { |
| 74 | try { | 79 | try { |
| @@ -95,6 +100,11 @@ class CallViewModel( | @@ -95,6 +100,11 @@ class CallViewModel( | ||
| 95 | room.events.collect { | 100 | room.events.collect { |
| 96 | when (it) { | 101 | when (it) { |
| 97 | is RoomEvent.FailedToConnect -> mutableError.value = it.error | 102 | is RoomEvent.FailedToConnect -> mutableError.value = it.error |
| 103 | + is RoomEvent.DataReceived -> { | ||
| 104 | + val identity = it.participant.identity ?: "" | ||
| 105 | + val message = it.data.toString(Charsets.UTF_8) | ||
| 106 | + mutableDataReceived.emit("$identity: $message") | ||
| 107 | + } | ||
| 98 | } | 108 | } |
| 99 | } | 109 | } |
| 100 | } | 110 | } |
| @@ -186,8 +196,15 @@ class CallViewModel( | @@ -186,8 +196,15 @@ class CallViewModel( | ||
| 186 | fun dismissError() { | 196 | fun dismissError() { |
| 187 | mutableError.value = null | 197 | mutableError.value = null |
| 188 | } | 198 | } |
| 199 | + | ||
| 200 | + fun sendData(message: String) { | ||
| 201 | + viewModelScope.launch { | ||
| 202 | + room.value?.localParticipant?.publishData(message.toByteArray(Charsets.UTF_8)) | ||
| 203 | + } | ||
| 204 | + } | ||
| 189 | } | 205 | } |
| 190 | 206 | ||
| 191 | private fun <T> LiveData<T>.hide(): LiveData<T> = this | 207 | private fun <T> LiveData<T>.hide(): LiveData<T> = this |
| 192 | 208 | ||
| 193 | -private fun <T> MutableStateFlow<T>.hide(): StateFlow<T> = this | ||
| 209 | +private fun <T> MutableStateFlow<T>.hide(): StateFlow<T> = this | ||
| 210 | +private fun <T> Flow<T>.hide(): Flow<T> = this |
| 1 | +<vector xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 2 | + android:width="24dp" | ||
| 3 | + android:height="24dp" | ||
| 4 | + android:viewportWidth="24" | ||
| 5 | + android:viewportHeight="24" | ||
| 6 | + android:tint="?attr/colorControlNormal" | ||
| 7 | + android:autoMirrored="true"> | ||
| 8 | + <path | ||
| 9 | + android:fillColor="@android:color/white" | ||
| 10 | + android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM6,9h12v2L6,11L6,9zM14,14L6,14v-2h8v2zM18,8L6,8L6,6h12v2z"/> | ||
| 11 | +</vector> |
| @@ -86,15 +86,6 @@ class CallActivity : AppCompatActivity() { | @@ -86,15 +86,6 @@ class CallActivity : AppCompatActivity() { | ||
| 86 | Timber.v { "Audio focus request failed" } | 86 | Timber.v { "Audio focus request failed" } |
| 87 | } | 87 | } |
| 88 | 88 | ||
| 89 | - lifecycleScope.launchWhenStarted { | ||
| 90 | - viewModel.error.collect { | ||
| 91 | - if(it != null){ | ||
| 92 | - Toast.makeText(this@CallActivity, "Error: $it", Toast.LENGTH_LONG).show() | ||
| 93 | - viewModel.dismissError() | ||
| 94 | - } | ||
| 95 | - } | ||
| 96 | - } | ||
| 97 | - | ||
| 98 | // Setup compose view. | 89 | // Setup compose view. |
| 99 | setContent { | 90 | setContent { |
| 100 | val room by viewModel.room.collectAsState() | 91 | val room by viewModel.room.collectAsState() |
| @@ -114,11 +105,30 @@ class CallActivity : AppCompatActivity() { | @@ -114,11 +105,30 @@ class CallActivity : AppCompatActivity() { | ||
| 114 | videoEnabled, | 105 | videoEnabled, |
| 115 | flipButtonEnabled, | 106 | flipButtonEnabled, |
| 116 | screencastEnabled, | 107 | screencastEnabled, |
| 117 | - onExitClick = { finish() } | 108 | + onExitClick = { finish() }, |
| 109 | + onSendMessage = { viewModel.sendData(it) } | ||
| 118 | ) | 110 | ) |
| 119 | } | 111 | } |
| 120 | } | 112 | } |
| 121 | 113 | ||
| 114 | + override fun onResume() { | ||
| 115 | + super.onResume() | ||
| 116 | + lifecycleScope.launchWhenResumed { | ||
| 117 | + viewModel.error.collect { | ||
| 118 | + if (it != null) { | ||
| 119 | + Toast.makeText(this@CallActivity, "Error: $it", Toast.LENGTH_LONG).show() | ||
| 120 | + viewModel.dismissError() | ||
| 121 | + } | ||
| 122 | + } | ||
| 123 | + } | ||
| 124 | + | ||
| 125 | + lifecycleScope.launchWhenResumed { | ||
| 126 | + viewModel.dataReceived.collect { | ||
| 127 | + Toast.makeText(this@CallActivity, "Data received: $it", Toast.LENGTH_LONG).show() | ||
| 128 | + } | ||
| 129 | + } | ||
| 130 | + } | ||
| 131 | + | ||
| 122 | private fun requestMediaProjection() { | 132 | private fun requestMediaProjection() { |
| 123 | val mediaProjectionManager = | 133 | val mediaProjectionManager = |
| 124 | getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager | 134 | getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager |
| @@ -141,7 +151,8 @@ class CallActivity : AppCompatActivity() { | @@ -141,7 +151,8 @@ class CallActivity : AppCompatActivity() { | ||
| 141 | screencastEnabled: Boolean = false, | 151 | screencastEnabled: Boolean = false, |
| 142 | onExitClick: () -> Unit = {}, | 152 | onExitClick: () -> Unit = {}, |
| 143 | error: Throwable? = null, | 153 | error: Throwable? = null, |
| 144 | - onSnackbarDismiss: () -> Unit = {} | 154 | + onSnackbarDismiss: () -> Unit = {}, |
| 155 | + onSendMessage: (String) -> Unit = {}, | ||
| 145 | ) { | 156 | ) { |
| 146 | AppTheme(darkTheme = true) { | 157 | AppTheme(darkTheme = true) { |
| 147 | ConstraintLayout( | 158 | ConstraintLayout( |
| @@ -216,7 +227,8 @@ class CallActivity : AppCompatActivity() { | @@ -216,7 +227,8 @@ class CallActivity : AppCompatActivity() { | ||
| 216 | Surface( | 227 | Surface( |
| 217 | onClick = { viewModel.setMicEnabled(!micEnabled) }, | 228 | onClick = { viewModel.setMicEnabled(!micEnabled) }, |
| 218 | indication = rememberRipple(false), | 229 | indication = rememberRipple(false), |
| 219 | - modifier = Modifier.size(controlSize) | 230 | + modifier = Modifier |
| 231 | + .size(controlSize) | ||
| 220 | .padding(controlPadding) | 232 | .padding(controlPadding) |
| 221 | ) { | 233 | ) { |
| 222 | val resource = | 234 | val resource = |
| @@ -230,7 +242,8 @@ class CallActivity : AppCompatActivity() { | @@ -230,7 +242,8 @@ class CallActivity : AppCompatActivity() { | ||
| 230 | Surface( | 242 | Surface( |
| 231 | onClick = { viewModel.setCameraEnabled(!videoEnabled) }, | 243 | onClick = { viewModel.setCameraEnabled(!videoEnabled) }, |
| 232 | indication = rememberRipple(false), | 244 | indication = rememberRipple(false), |
| 233 | - modifier = Modifier.size(controlSize) | 245 | + modifier = Modifier |
| 246 | + .size(controlSize) | ||
| 234 | .padding(controlPadding) | 247 | .padding(controlPadding) |
| 235 | ) { | 248 | ) { |
| 236 | val resource = | 249 | val resource = |
| @@ -244,7 +257,8 @@ class CallActivity : AppCompatActivity() { | @@ -244,7 +257,8 @@ class CallActivity : AppCompatActivity() { | ||
| 244 | Surface( | 257 | Surface( |
| 245 | onClick = { viewModel.flipCamera() }, | 258 | onClick = { viewModel.flipCamera() }, |
| 246 | indication = rememberRipple(false), | 259 | indication = rememberRipple(false), |
| 247 | - modifier = Modifier.size(controlSize) | 260 | + modifier = Modifier |
| 261 | + .size(controlSize) | ||
| 248 | .padding(controlPadding) | 262 | .padding(controlPadding) |
| 249 | ) { | 263 | ) { |
| 250 | Icon( | 264 | Icon( |
| @@ -262,7 +276,8 @@ class CallActivity : AppCompatActivity() { | @@ -262,7 +276,8 @@ class CallActivity : AppCompatActivity() { | ||
| 262 | } | 276 | } |
| 263 | }, | 277 | }, |
| 264 | indication = rememberRipple(false), | 278 | indication = rememberRipple(false), |
| 265 | - modifier = Modifier.size(controlSize) | 279 | + modifier = Modifier |
| 280 | + .size(controlSize) | ||
| 266 | .padding(controlPadding) | 281 | .padding(controlPadding) |
| 267 | ) { | 282 | ) { |
| 268 | val resource = | 283 | val resource = |
| @@ -273,10 +288,65 @@ class CallActivity : AppCompatActivity() { | @@ -273,10 +288,65 @@ class CallActivity : AppCompatActivity() { | ||
| 273 | tint = Color.White, | 288 | tint = Color.White, |
| 274 | ) | 289 | ) |
| 275 | } | 290 | } |
| 291 | + | ||
| 292 | + var showMessageDialog by remember { mutableStateOf(false) } | ||
| 293 | + var messageToSend by remember { mutableStateOf("") } | ||
| 294 | + Surface( | ||
| 295 | + onClick = { showMessageDialog = true }, | ||
| 296 | + indication = rememberRipple(false), | ||
| 297 | + modifier = Modifier | ||
| 298 | + .size(controlSize) | ||
| 299 | + .padding(controlPadding) | ||
| 300 | + ) { | ||
| 301 | + Icon( | ||
| 302 | + painterResource(id = R.drawable.baseline_chat_24), | ||
| 303 | + contentDescription = "Send Message", | ||
| 304 | + tint = Color.White, | ||
| 305 | + ) | ||
| 306 | + } | ||
| 307 | + | ||
| 308 | + if (showMessageDialog) { | ||
| 309 | + AlertDialog( | ||
| 310 | + onDismissRequest = { | ||
| 311 | + showMessageDialog = false | ||
| 312 | + messageToSend = "" | ||
| 313 | + }, | ||
| 314 | + title = { | ||
| 315 | + Text(text = "Send Message") | ||
| 316 | + }, | ||
| 317 | + text = { | ||
| 318 | + OutlinedTextField( | ||
| 319 | + value = messageToSend, | ||
| 320 | + onValueChange = { messageToSend = it }, | ||
| 321 | + label = { Text("Message") }, | ||
| 322 | + modifier = Modifier.fillMaxWidth(), | ||
| 323 | + ) | ||
| 324 | + }, | ||
| 325 | + confirmButton = { | ||
| 326 | + Button( | ||
| 327 | + onClick = { | ||
| 328 | + onSendMessage(messageToSend) | ||
| 329 | + showMessageDialog = false | ||
| 330 | + messageToSend = "" | ||
| 331 | + } | ||
| 332 | + ) { Text("Send") } | ||
| 333 | + }, | ||
| 334 | + dismissButton = { | ||
| 335 | + Button( | ||
| 336 | + onClick = { | ||
| 337 | + showMessageDialog = false | ||
| 338 | + messageToSend = "" | ||
| 339 | + } | ||
| 340 | + ) { Text("Cancel") } | ||
| 341 | + }, | ||
| 342 | + backgroundColor = Color.Black, | ||
| 343 | + ) | ||
| 344 | + } | ||
| 276 | Surface( | 345 | Surface( |
| 277 | onClick = { onExitClick() }, | 346 | onClick = { onExitClick() }, |
| 278 | indication = rememberRipple(false), | 347 | indication = rememberRipple(false), |
| 279 | - modifier = Modifier.size(controlSize) | 348 | + modifier = Modifier |
| 349 | + .size(controlSize) | ||
| 280 | .padding(controlPadding) | 350 | .padding(controlPadding) |
| 281 | ) { | 351 | ) { |
| 282 | Icon( | 352 | Icon( |
| @@ -290,7 +360,7 @@ class CallActivity : AppCompatActivity() { | @@ -290,7 +360,7 @@ class CallActivity : AppCompatActivity() { | ||
| 290 | // Snack bar for errors | 360 | // Snack bar for errors |
| 291 | val scaffoldState = rememberScaffoldState() | 361 | val scaffoldState = rememberScaffoldState() |
| 292 | val scope = rememberCoroutineScope() | 362 | val scope = rememberCoroutineScope() |
| 293 | - if(error != null) { | 363 | + if (error != null) { |
| 294 | Scaffold( | 364 | Scaffold( |
| 295 | scaffoldState = scaffoldState, | 365 | scaffoldState = scaffoldState, |
| 296 | floatingActionButton = { | 366 | floatingActionButton = { |
| @@ -307,7 +377,10 @@ class CallActivity : AppCompatActivity() { | @@ -307,7 +377,10 @@ class CallActivity : AppCompatActivity() { | ||
| 307 | content = { innerPadding -> | 377 | content = { innerPadding -> |
| 308 | Text( | 378 | Text( |
| 309 | text = "Body content", | 379 | text = "Body content", |
| 310 | - modifier = Modifier.padding(innerPadding).fillMaxSize().wrapContentSize() | 380 | + modifier = Modifier |
| 381 | + .padding(innerPadding) | ||
| 382 | + .fillMaxSize() | ||
| 383 | + .wrapContentSize() | ||
| 311 | ) | 384 | ) |
| 312 | } | 385 | } |
| 313 | ) | 386 | ) |
| 1 | package io.livekit.android.sample | 1 | package io.livekit.android.sample |
| 2 | 2 | ||
| 3 | import android.app.Activity | 3 | import android.app.Activity |
| 4 | +import android.content.DialogInterface | ||
| 4 | import android.media.AudioManager | 5 | import android.media.AudioManager |
| 5 | import android.media.projection.MediaProjectionManager | 6 | import android.media.projection.MediaProjectionManager |
| 6 | import android.os.Bundle | 7 | import android.os.Bundle |
| 7 | import android.os.Parcelable | 8 | import android.os.Parcelable |
| 8 | import android.view.View | 9 | import android.view.View |
| 10 | +import android.widget.EditText | ||
| 9 | import android.widget.Toast | 11 | import android.widget.Toast |
| 10 | import androidx.activity.result.contract.ActivityResultContracts | 12 | import androidx.activity.result.contract.ActivityResultContracts |
| 13 | +import androidx.appcompat.app.AlertDialog | ||
| 11 | import androidx.appcompat.app.AppCompatActivity | 14 | import androidx.appcompat.app.AppCompatActivity |
| 12 | import androidx.lifecycle.lifecycleScope | 15 | import androidx.lifecycle.lifecycleScope |
| 13 | import androidx.recyclerview.widget.LinearLayoutManager | 16 | import androidx.recyclerview.widget.LinearLayoutManager |
| @@ -72,15 +75,6 @@ class CallActivity : AppCompatActivity() { | @@ -72,15 +75,6 @@ class CallActivity : AppCompatActivity() { | ||
| 72 | } | 75 | } |
| 73 | } | 76 | } |
| 74 | 77 | ||
| 75 | - lifecycleScope.launchWhenStarted { | ||
| 76 | - viewModel.error.collect { | ||
| 77 | - if(it != null){ | ||
| 78 | - Toast.makeText(this@CallActivity, "Error: $it", Toast.LENGTH_LONG).show() | ||
| 79 | - viewModel.dismissError() | ||
| 80 | - } | ||
| 81 | - } | ||
| 82 | - } | ||
| 83 | - | ||
| 84 | // speaker view setup | 78 | // speaker view setup |
| 85 | lifecycleScope.launchWhenCreated { | 79 | lifecycleScope.launchWhenCreated { |
| 86 | viewModel.room.filterNotNull().take(1) | 80 | viewModel.room.filterNotNull().take(1) |
| @@ -161,6 +155,19 @@ class CallActivity : AppCompatActivity() { | @@ -161,6 +155,19 @@ class CallActivity : AppCompatActivity() { | ||
| 161 | ) | 155 | ) |
| 162 | } | 156 | } |
| 163 | 157 | ||
| 158 | + binding.message.setOnClickListener { | ||
| 159 | + val editText = EditText(this) | ||
| 160 | + AlertDialog.Builder(this) | ||
| 161 | + .setTitle("Send Message") | ||
| 162 | + .setView(editText) | ||
| 163 | + .setPositiveButton("Send") { dialog, _ -> | ||
| 164 | + viewModel.sendData(editText.text?.toString() ?: "") | ||
| 165 | + } | ||
| 166 | + .setNegativeButton("Cancel") { _, _ -> } | ||
| 167 | + .create() | ||
| 168 | + .show() | ||
| 169 | + } | ||
| 170 | + | ||
| 164 | binding.exit.setOnClickListener { finish() } | 171 | binding.exit.setOnClickListener { finish() } |
| 165 | 172 | ||
| 166 | // Grab audio focus for video call | 173 | // Grab audio focus for video call |
| @@ -184,6 +191,24 @@ class CallActivity : AppCompatActivity() { | @@ -184,6 +191,24 @@ class CallActivity : AppCompatActivity() { | ||
| 184 | } | 191 | } |
| 185 | } | 192 | } |
| 186 | 193 | ||
| 194 | + override fun onResume() { | ||
| 195 | + super.onResume() | ||
| 196 | + lifecycleScope.launchWhenResumed { | ||
| 197 | + viewModel.error.collect { | ||
| 198 | + if (it != null) { | ||
| 199 | + Toast.makeText(this@CallActivity, "Error: $it", Toast.LENGTH_LONG).show() | ||
| 200 | + viewModel.dismissError() | ||
| 201 | + } | ||
| 202 | + } | ||
| 203 | + } | ||
| 204 | + | ||
| 205 | + lifecycleScope.launchWhenResumed { | ||
| 206 | + viewModel.dataReceived.collect { | ||
| 207 | + Toast.makeText(this@CallActivity, "Data received: $it", Toast.LENGTH_LONG).show() | ||
| 208 | + } | ||
| 209 | + } | ||
| 210 | + } | ||
| 211 | + | ||
| 187 | private fun requestMediaProjection() { | 212 | private fun requestMediaProjection() { |
| 188 | val mediaProjectionManager = | 213 | val mediaProjectionManager = |
| 189 | getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager | 214 | getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager |
| @@ -10,9 +10,9 @@ | @@ -10,9 +10,9 @@ | ||
| 10 | android:layout_height="0dp" | 10 | android:layout_height="0dp" |
| 11 | android:background="@color/no_video_background" | 11 | android:background="@color/no_video_background" |
| 12 | app:layout_constraintBottom_toTopOf="@id/audience_row" | 12 | app:layout_constraintBottom_toTopOf="@id/audience_row" |
| 13 | - app:layout_constraintTop_toTopOf="parent" | ||
| 14 | app:layout_constraintEnd_toEndOf="parent" | 13 | app:layout_constraintEnd_toEndOf="parent" |
| 15 | - app:layout_constraintStart_toStartOf="parent"> | 14 | + app:layout_constraintStart_toStartOf="parent" |
| 15 | + app:layout_constraintTop_toTopOf="parent"> | ||
| 16 | 16 | ||
| 17 | <ImageView | 17 | <ImageView |
| 18 | android:layout_width="120dp" | 18 | android:layout_width="120dp" |
| @@ -105,6 +105,14 @@ | @@ -105,6 +105,14 @@ | ||
| 105 | android:src="@drawable/baseline_cast_24" /> | 105 | android:src="@drawable/baseline_cast_24" /> |
| 106 | 106 | ||
| 107 | <ImageView | 107 | <ImageView |
| 108 | + android:id="@+id/message" | ||
| 109 | + android:layout_width="@dimen/control_size" | ||
| 110 | + android:layout_height="@dimen/control_size" | ||
| 111 | + android:background="?android:attr/selectableItemBackground" | ||
| 112 | + android:padding="@dimen/control_padding" | ||
| 113 | + android:src="@drawable/baseline_chat_24" /> | ||
| 114 | + | ||
| 115 | + <ImageView | ||
| 108 | android:id="@+id/exit" | 116 | android:id="@+id/exit" |
| 109 | android:layout_width="@dimen/control_size" | 117 | android:layout_width="@dimen/control_size" |
| 110 | android:layout_height="@dimen/control_size" | 118 | android:layout_height="@dimen/control_size" |
-
请 注册 或 登录 后发表评论