davidliu
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
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"