From 5eceb1ce9474334c9e1556b6284b5cf74490faa6 Mon Sep 17 00:00:00 2001 From: Daniel Novak <1726289+DanielNovak@users.noreply.github.com> Date: Fri, 25 Aug 2023 15:37:47 +0200 Subject: [PATCH 01/12] Initialise default mediamanager values on call.join (#798) --- .../api/stream-video-android-core.api | 1 + .../io/getstream/video/android/core/Call.kt | 47 +++++++++++++++++++ .../video/android/core/MediaManager.kt | 10 ++++ .../video/android/core/call/RtcSession.kt | 27 ----------- 4 files changed, 58 insertions(+), 27 deletions(-) diff --git a/stream-video-android-core/api/stream-video-android-core.api b/stream-video-android-core/api/stream-video-android-core.api index 46e9a90c44..e3973462ea 100644 --- a/stream-video-android-core/api/stream-video-android-core.api +++ b/stream-video-android-core/api/stream-video-android-core.api @@ -233,6 +233,7 @@ public final class io/getstream/video/android/core/CameraManager { public static synthetic fun resume$default (Lio/getstream/video/android/core/CameraManager;ZILjava/lang/Object;)V public final fun select (Ljava/lang/String;Z)V public static synthetic fun select$default (Lio/getstream/video/android/core/CameraManager;Ljava/lang/String;ZILjava/lang/Object;)V + public final fun setDirection (Lio/getstream/video/android/core/CameraDirection;)V public final fun setEnabled (ZZ)V public static synthetic fun setEnabled$default (Lio/getstream/video/android/core/CameraManager;ZZILjava/lang/Object;)V } diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt index f6e21079ff..6f33cc5054 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt @@ -46,8 +46,10 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import org.openapitools.client.models.AcceptCallResponse +import org.openapitools.client.models.AudioSettings import org.openapitools.client.models.BlockUserResponse import org.openapitools.client.models.CallSettingsRequest +import org.openapitools.client.models.CallSettingsResponse import org.openapitools.client.models.GetCallResponse import org.openapitools.client.models.GetOrCreateCallResponse import org.openapitools.client.models.GoLiveResponse @@ -65,6 +67,7 @@ import org.openapitools.client.models.UpdateCallRequest import org.openapitools.client.models.UpdateCallResponse import org.openapitools.client.models.UpdateUserPermissionsResponse import org.openapitools.client.models.VideoEvent +import org.openapitools.client.models.VideoSettings import org.threeten.bp.OffsetDateTime import org.webrtc.RendererCommon import org.webrtc.audio.JavaAudioDeviceModule.AudioSamples @@ -264,6 +267,18 @@ public class Call( while (retryCount < 3) { result = _join(create, createOptions, ring, notify) if (result is Success) { + // we initialise the camera, mic and other according to local + backend settings + // only when the call is joined to make sure we don't switch and override + // the settings during a call. + val settings = state.settings.value + if (settings != null) { + updateMediaManagerFromSettings(settings) + } else { + logger.w { + "[join] Call settings were null - this should never happen after a call" + + "is joined. MediaManager will not be initialised with server settings." + } + } return result } if (result is Failure) { @@ -734,6 +749,38 @@ public class Call( } } + private fun updateMediaManagerFromSettings(callSettings: CallSettingsResponse) { + // Speaker + if (speaker.status.value is DeviceStatus.NotSelected) { + val enableSpeaker = if (callSettings.video.cameraDefaultOn || camera.status.value is DeviceStatus.Enabled) { + // if camera is enabled then enable speaker. Eventually this should + // be a new audio.defaultDevice setting returned from backend + true + } else { + callSettings.audio.defaultDevice == AudioSettings.DefaultDevice.Speaker + } + speaker.setEnabled(enableSpeaker) + } + + // Camera + if (camera.status.value is DeviceStatus.NotSelected) { + val defaultDirection = + if (callSettings.video.cameraFacing == VideoSettings.CameraFacing.Front) { + CameraDirection.Front + } else { + CameraDirection.Back + } + camera.setDirection(defaultDirection) + camera.setEnabled(callSettings.video.cameraDefaultOn) + } + + // Mic + if (microphone.status.value == DeviceStatus.NotSelected) { + val enabled = callSettings.audio.micDefaultOn + microphone.setEnabled(enabled) + } + } + suspend fun listRecordings(): Result { return clientImpl.listRecordings(type, id, "what") } diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.kt index a744bff5c3..1ac2f2ce4a 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.kt @@ -401,6 +401,16 @@ public class CameraManager( } } + fun setDirection(cameraDirection: CameraDirection) { + val previousDirection = _direction.value + _direction.value = cameraDirection + + // flip camera if necessary + if (previousDirection != cameraDirection) { + flip() + } + } + /** * Flips the camera */ diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt index deec4a14b6..31ce1f676e 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt @@ -22,7 +22,6 @@ import io.getstream.result.Result import io.getstream.result.Result.Failure import io.getstream.result.Result.Success import io.getstream.video.android.core.Call -import io.getstream.video.android.core.CameraDirection import io.getstream.video.android.core.DeviceStatus import io.getstream.video.android.core.StreamVideo import io.getstream.video.android.core.StreamVideoImpl @@ -77,7 +76,6 @@ import kotlinx.serialization.json.Json import okio.IOException import org.openapitools.client.models.OwnCapability import org.openapitools.client.models.VideoEvent -import org.openapitools.client.models.VideoSettings import org.webrtc.MediaConstraints import org.webrtc.MediaStream import org.webrtc.MediaStreamTrack @@ -529,31 +527,6 @@ public class RtcSession internal constructor( // step 2 ensure all tracks are setup correctly // start capturing the video - // if there is no preview and the camera hasn't been selected by the user fallback to settings - if (call.mediaManager.camera.status.value == DeviceStatus.NotSelected) { - val enabled = settings?.video?.cameraDefaultOn == true - call.mediaManager.camera.setEnabled(enabled) - if (enabled) { - // check the settings if we should default to front or back facing camera - val defaultDirection = - if (settings?.video?.cameraFacing == VideoSettings.CameraFacing.Front) { - CameraDirection.Front - } else { - CameraDirection.Back - } - // TODO: would be nicer to initialize the camera on the right device to begin with - if (defaultDirection != call.mediaManager.camera.direction.value) { - call.mediaManager.camera.flip() - } - } - } - - // if there is no preview and the microphone hasn't been selected by the user fallback to settings - if (call.mediaManager.microphone.status.value == DeviceStatus.NotSelected) { - val enabled = settings?.audio?.micDefaultOn == true - call.mediaManager.microphone.setEnabled(enabled) - } - timer.split("media enabled") // step 4 add the audio track to the publisher setLocalTrack( From 7eb3b237811c1090a1ff98d49e2e792ee79a9f7d Mon Sep 17 00:00:00 2001 From: Jaewoong Eum Date: Tue, 29 Aug 2023 09:22:03 +0900 Subject: [PATCH 02/12] Refactor call lobby screen and viewmodel (#800) --- .../video/android/ui/lobby/CallLobbyScreen.kt | 17 ++++------------- .../android/ui/lobby/CallLobbyViewModel.kt | 9 ++++----- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/lobby/CallLobbyScreen.kt b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/lobby/CallLobbyScreen.kt index f87b9e8f08..7ee968a110 100644 --- a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/lobby/CallLobbyScreen.kt +++ b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/lobby/CallLobbyScreen.kt @@ -231,26 +231,20 @@ private fun CallLobbyBody( isMicrophoneEnabled = isMicrophoneEnabled, onCallAction = { action -> when (action) { - is ToggleCamera -> callLobbyViewModel.enableCamera(!isCameraEnabled) - is ToggleMicrophone -> callLobbyViewModel.enableMicrophone(!isMicrophoneEnabled) + is ToggleCamera -> callLobbyViewModel.enableCamera(action.isEnabled) + is ToggleMicrophone -> callLobbyViewModel.enableMicrophone(action.isEnabled) else -> Unit } }, ) - LobbyDescription( - callLobbyViewModel = callLobbyViewModel, - cameraEnabled = isCameraEnabled, - microphoneEnabled = isMicrophoneEnabled, - ) + LobbyDescription(callLobbyViewModel = callLobbyViewModel) } } @Composable private fun LobbyDescription( callLobbyViewModel: CallLobbyViewModel, - cameraEnabled: Boolean, - microphoneEnabled: Boolean, ) { val session by callLobbyViewModel.call.state.session.collectAsState() @@ -280,10 +274,7 @@ private fun LobbyDescription( text = stringResource(id = R.string.join_call), onClick = { callLobbyViewModel.handleUiEvent( - CallLobbyEvent.JoinCall( - cameraEnabled = cameraEnabled, - microphoneEnabled = microphoneEnabled, - ), + CallLobbyEvent.JoinCall, ) }, ) diff --git a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/lobby/CallLobbyViewModel.kt b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/lobby/CallLobbyViewModel.kt index 72c2e86dc6..a56c6e4465 100644 --- a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/lobby/CallLobbyViewModel.kt +++ b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/lobby/CallLobbyViewModel.kt @@ -144,10 +144,10 @@ class CallLobbyViewModel @Inject constructor( is CallLobbyEvent.JoinCall -> { flowOf(CallLobbyUiState.JoinCompleted) } + is CallLobbyEvent.JoinFailed -> { flowOf(CallLobbyUiState.JoinFailed(event.reason)) } - else -> flowOf(CallLobbyUiState.Nothing) } } .onCompletion { _isLoading.value = false } @@ -178,17 +178,16 @@ class CallLobbyViewModel @Inject constructor( } sealed interface CallLobbyUiState { - object Nothing : CallLobbyUiState + data object Nothing : CallLobbyUiState - object JoinCompleted : CallLobbyUiState + data object JoinCompleted : CallLobbyUiState data class JoinFailed(val reason: String?) : CallLobbyUiState } sealed interface CallLobbyEvent { - object Nothing : CallLobbyEvent - class JoinCall(val cameraEnabled: Boolean, val microphoneEnabled: Boolean) : CallLobbyEvent + data object JoinCall : CallLobbyEvent data class JoinFailed(val reason: String?) : CallLobbyEvent } From b88b8df62cc68284c77bab534f1c5cc380f7f863 Mon Sep 17 00:00:00 2001 From: Daniel Novak <1726289+DanielNovak@users.noreply.github.com> Date: Tue, 29 Aug 2023 15:42:24 +0200 Subject: [PATCH 03/12] Prevent crash if MediaStreamTrack is already disposed (#802) --- .../compose/ui/components/video/VideoRenderer.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/video/VideoRenderer.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/video/VideoRenderer.kt index 5220d14bfb..f84ce840a4 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/video/VideoRenderer.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/video/VideoRenderer.kt @@ -148,7 +148,14 @@ private fun cleanTrack( mediaTrack: MediaTrack?, ) { if (view != null && mediaTrack is VideoTrack) { - mediaTrack.video.removeSink(view) + try { + mediaTrack.video.removeSink(view) + } catch (e: Exception) { + // The MediaStreamTrack can be already disposed at this point (from other parts of the code) + // Removing the Sink at this point will throw a IllegalStateException("MediaStreamTrack has been disposed.") + // See MediaStreamTrack.checkMediaStreamTrackExists() + StreamLog.w("VideoRenderer") { "Failed to removeSink in onDispose: ${e.message}" } + } } } @@ -163,7 +170,7 @@ private fun setupVideo( mediaTrack.video.addSink(renderer) } } catch (e: Exception) { - StreamLog.d("VideoRenderer") { e.message.toString() } + StreamLog.w("VideoRenderer") { e.message.toString() } } } From 0d0960b66d5828fcbcfa1c68297d1ab624f35ecd Mon Sep 17 00:00:00 2001 From: Daniel Novak <1726289+DanielNovak@users.noreply.github.com> Date: Wed, 30 Aug 2023 07:42:11 +0200 Subject: [PATCH 04/12] Scan QR code button in demo (#799) * Scan QR code button in demo * Add Done key handling --------- Co-authored-by: Jaewoong Eum --- dogfooding/build.gradle.kts | 4 + .../video/android/ui/join/CallJoinScreen.kt | 74 ++++++++++++++++++- .../src/main/res/drawable/ic_scan_qr.xml | 25 +++++++ dogfooding/src/main/res/values/strings.xml | 1 + gradle/libs.versions.toml | 2 + 5 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 dogfooding/src/main/res/drawable/ic_scan_qr.xml diff --git a/dogfooding/build.gradle.kts b/dogfooding/build.gradle.kts index 47b23117c2..1d6576d547 100644 --- a/dogfooding/build.gradle.kts +++ b/dogfooding/build.gradle.kts @@ -241,6 +241,7 @@ dependencies { implementation(libs.androidx.compose.material.iconsExtended) implementation(libs.androidx.hilt.navigation) implementation(libs.landscapist.coil) + implementation(libs.accompanist.permission) // hilt implementation(libs.hilt.android) @@ -252,6 +253,9 @@ dependencies { // Play Install Referrer library - used to extract the meeting link from demo flow after install implementation(libs.play.install.referrer) + // Only used for launching a QR code scanner in demo app + implementation(libs.play.code.scanner) + // memory detection debugImplementation(libs.leakCanary) diff --git a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/join/CallJoinScreen.kt b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/join/CallJoinScreen.kt index c3de21883b..9569050184 100644 --- a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/join/CallJoinScreen.kt +++ b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/join/CallJoinScreen.kt @@ -18,6 +18,8 @@ package io.getstream.video.android.ui.join +import android.net.Uri +import android.widget.Toast import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -36,9 +38,12 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Icon +import androidx.compose.material.IconButton import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.material.TextField @@ -67,7 +72,12 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel +import com.google.android.gms.tasks.OnSuccessListener +import com.google.mlkit.vision.barcode.common.Barcode +import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions +import com.google.mlkit.vision.codescanner.GmsBarcodeScanning import io.getstream.video.android.BuildConfig +import io.getstream.video.android.DeeplinkingActivity import io.getstream.video.android.R import io.getstream.video.android.compose.theme.VideoTheme import io.getstream.video.android.compose.ui.components.avatar.UserAvatar @@ -86,6 +96,8 @@ fun CallJoinScreen( ) { val uiState by callJoinViewModel.uiState.collectAsState(CallJoinUiState.Nothing) val isLoggedOut by callJoinViewModel.isLoggedOut.collectAsState(initial = false) + val qrCodeCallback = rememberQrCodeCallback() + val context = LocalContext.current HandleCallJoinUiState( callJoinUiState = uiState, @@ -111,6 +123,12 @@ fun CallJoinScreen( .verticalScroll(rememberScrollState()) .weight(1f), callJoinViewModel = callJoinViewModel, + openCamera = { + val options = GmsBarcodeScannerOptions.Builder() + .setBarcodeFormats(Barcode.FORMAT_QR_CODE, Barcode.FORMAT_AZTEC).build() + val scanner = GmsBarcodeScanning.getClient(context, options) + scanner.startScan().addOnSuccessListener(qrCodeCallback) + }, ) } @@ -174,6 +192,7 @@ private fun CallJoinHeader( @Composable private fun CallJoinBody( modifier: Modifier, + openCamera: () -> Unit, callJoinViewModel: CallJoinViewModel = hiltViewModel(), ) { val user by if (LocalInspectionMode.current) { @@ -217,7 +236,7 @@ private fun CallJoinBody( color = Colors.description, textAlign = TextAlign.Center, fontSize = 18.sp, - modifier = Modifier.widthIn(0.dp, 350.dp), + modifier = Modifier.widthIn(0.dp, 320.dp), ) Spacer(modifier = Modifier.height(42.dp)) @@ -258,7 +277,24 @@ private fun CallJoinBody( ), shape = RoundedCornerShape(6.dp), value = callId, + singleLine = true, onValueChange = { callId = it }, + trailingIcon = { + IconButton( + onClick = openCamera, + modifier = Modifier.fillMaxHeight(), + content = { + Icon( + painter = painterResource(id = R.drawable.ic_scan_qr), + contentDescription = stringResource( + id = R.string.join_call_by_qr_code, + ), + tint = Colors.description, + modifier = Modifier.size(36.dp), + ) + }, + ) + }, colors = TextFieldDefaults.textFieldColors( textColor = Color.White, focusedLabelColor = VideoTheme.colors.primaryAccent, @@ -275,6 +311,11 @@ private fun CallJoinBody( color = Color(0xFF5D6168), ) }, + keyboardActions = KeyboardActions( + onDone = { + callJoinViewModel.handleUiEvent(CallJoinEvent.JoinCall(callId = callId)) + }, + ), ) StreamButton( @@ -333,6 +374,37 @@ private fun HandleCallJoinUiState( } } +@Composable +private fun rememberQrCodeCallback(): OnSuccessListener { + val context = LocalContext.current + + return remember { + OnSuccessListener { + val url = it.url?.url + val callId = if (url != null) { + val id = Uri.parse(url).getQueryParameter("id") + if (!id.isNullOrEmpty()) { + id + } else { + null + } + } else { + null + } + + if (!callId.isNullOrEmpty()) { + context.startActivity(DeeplinkingActivity.createIntent(context, callId)) + } else { + Toast.makeText( + context, + "Unrecognised meeting QR code format", + Toast.LENGTH_SHORT, + ).show() + } + } + } +} + @Preview @Composable private fun CallJoinScreenPreview() { diff --git a/dogfooding/src/main/res/drawable/ic_scan_qr.xml b/dogfooding/src/main/res/drawable/ic_scan_qr.xml new file mode 100644 index 0000000000..f32fff5f31 --- /dev/null +++ b/dogfooding/src/main/res/drawable/ic_scan_qr.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/dogfooding/src/main/res/values/strings.xml b/dogfooding/src/main/res/values/strings.xml index bcef6d77d3..0565c562f7 100644 --- a/dogfooding/src/main/res/values/strings.xml +++ b/dogfooding/src/main/res/values/strings.xml @@ -26,6 +26,7 @@ Sign out Build reliable video calling, audio rooms, and live streaming with our easy-to-use SDKs and global edge network Join Call + Scan QR meeting code You are about to join a call. %d more people are in the call. Don\'t have a Call ID? Call ID diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 80060e4cce..1ea4395187 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -65,6 +65,7 @@ firebaseBom = "32.1.0" firebaseCrashlytics = "2.9.5" installReferrer = "2.2" +playCodeScanner = "16.1.0" hilt = "2.46.1" desugar = "2.0.3" @@ -178,6 +179,7 @@ firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashly firebase-analyrics = { group = "com.google.firebase", name = "firebase-analytics-ktx" } play-install-referrer = { group = "com.android.installreferrer", name = "installreferrer", version.ref = "installReferrer" } +play-code-scanner = { group = "com.google.android.gms", name = "play-services-code-scanner", version.ref = "playCodeScanner" } robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" } leakCanary = { group = "com.squareup.leakcanary", name = "leakcanary-android", version.ref = "leakCanary" } From b67d16d2fcf0ce3314f7b0907fb98be8e0405695 Mon Sep 17 00:00:00 2001 From: Jaewoong Eum Date: Wed, 30 Aug 2023 14:52:57 +0900 Subject: [PATCH 05/12] Add paddings for ParticipantGrid (#803) --- .../api/stream-video-android-compose.api | 8 +++++--- .../getstream/video/android/compose/theme/StreamDimens.kt | 6 +++++- .../components/call/renderer/ParticipantsRegularGrid.kt | 5 ++++- .../internal/LandscapeScreenSharingVideoRenderer.kt | 4 +++- .../internal/PortraitScreenSharingVideoRenderer.kt | 4 +++- .../src/main/res/values/dimens.xml | 1 + 6 files changed, 21 insertions(+), 7 deletions(-) diff --git a/stream-video-android-compose/api/stream-video-android-compose.api b/stream-video-android-compose/api/stream-video-android-compose.api index aac1c53aba..71d6d3e0e5 100644 --- a/stream-video-android-compose/api/stream-video-android-compose.api +++ b/stream-video-android-compose/api/stream-video-android-compose.api @@ -146,7 +146,7 @@ public final class io/getstream/video/android/compose/theme/StreamColors$Compani public final class io/getstream/video/android/compose/theme/StreamDimens { public static final field $stable I public static final field Companion Lio/getstream/video/android/compose/theme/StreamDimens$Companion; - public synthetic fun (FFFFFFFFFFFJJJJFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (FFFFFFFFFFFJJJJFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFLkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1-D9Ej5fM ()F public final fun component10-D9Ej5fM ()F public final fun component11-D9Ej5fM ()F @@ -227,9 +227,10 @@ public final class io/getstream/video/android/compose/theme/StreamDimens { public final fun component8-D9Ej5fM ()F public final fun component80-D9Ej5fM ()F public final fun component81-D9Ej5fM ()F + public final fun component82-D9Ej5fM ()F public final fun component9-D9Ej5fM ()F - public final fun copy-W3031cc (FFFFFFFFFFFJJJJFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)Lio/getstream/video/android/compose/theme/StreamDimens; - public static synthetic fun copy-W3031cc$default (Lio/getstream/video/android/compose/theme/StreamDimens;FFFFFFFFFFFJJJJFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFIIILjava/lang/Object;)Lio/getstream/video/android/compose/theme/StreamDimens; + public final fun copy-uuJv5Cc (FFFFFFFFFFFJJJJFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)Lio/getstream/video/android/compose/theme/StreamDimens; + public static synthetic fun copy-uuJv5Cc$default (Lio/getstream/video/android/compose/theme/StreamDimens;FFFFFFFFFFFJJJJFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFIIILjava/lang/Object;)Lio/getstream/video/android/compose/theme/StreamDimens; public fun equals (Ljava/lang/Object;)Z public final fun getAudioAvatarPadding-D9Ej5fM ()F public final fun getAudioAvatarSize-D9Ej5fM ()F @@ -292,6 +293,7 @@ public final class io/getstream/video/android/compose/theme/StreamDimens { public final fun getParticipantLabelTextPaddingStart-D9Ej5fM ()F public final fun getParticipantScreenSharingFocusedBorderWidth-D9Ej5fM ()F public final fun getParticipantSoundIndicatorPadding-D9Ej5fM ()F + public final fun getParticipantsGridPadding-D9Ej5fM ()F public final fun getParticipantsInfoAvatarSize-D9Ej5fM ()F public final fun getParticipantsInfoMenuOptionsButtonHeight-D9Ej5fM ()F public final fun getParticipantsTextPadding-D9Ej5fM ()F diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/theme/StreamDimens.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/theme/StreamDimens.kt index 653fefdbc9..480990b1e8 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/theme/StreamDimens.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/theme/StreamDimens.kt @@ -80,6 +80,7 @@ public data class StreamDimens( public val participantInfoMenuOptionsHeight: Dp, public val participantsInfoMenuOptionsButtonHeight: Dp, public val participantsInfoAvatarSize: Dp, + public val participantsGridPadding: Dp, public val floatingVideoPadding: Dp, public val floatingVideoHeight: Dp, public val floatingVideoWidth: Dp, @@ -207,7 +208,10 @@ public data class StreamDimens( id = R.dimen.stream_video_callParticipantLabelTextPadding, ), participantLabelTextPaddingStart = dimensionResource( - id = R.dimen.stream_video_callParticipantSoundIndicatorPaddingStart, + id = R.dimen.stream_video_participantsGridPadding, + ), + participantsGridPadding = dimensionResource( + id = R.dimen.stream_video_audioRoomAvatarLandscapePadding, ), landscapeControlActionsButtonSize = dimensionResource( id = R.dimen.stream_video_landscapeControlActionsButtonSize, diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsRegularGrid.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsRegularGrid.kt index 3b4bf9963d..a2fb6da380 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsRegularGrid.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsRegularGrid.kt @@ -19,6 +19,7 @@ package io.getstream.video.android.compose.ui.components.call.renderer import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -67,7 +68,9 @@ public fun ParticipantsRegularGrid( var parentSize: IntSize by remember { mutableStateOf(IntSize(0, 0)) } Box( - modifier = modifier.background(color = VideoTheme.colors.appBackground), + modifier = modifier + .background(color = VideoTheme.colors.appBackground) + .padding(VideoTheme.dimens.participantsGridPadding), ) { val roomParticipants by call.state.participants.collectAsStateWithLifecycle() diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LandscapeScreenSharingVideoRenderer.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LandscapeScreenSharingVideoRenderer.kt index 42a812bbd0..f7c0dd89c0 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LandscapeScreenSharingVideoRenderer.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LandscapeScreenSharingVideoRenderer.kt @@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -87,7 +88,8 @@ internal fun LandscapeScreenSharingVideoRenderer( Box( modifier = Modifier .fillMaxHeight() - .weight(0.65f), + .weight(0.65f) + .padding(VideoTheme.dimens.participantsGridPadding), ) { ScreenShareVideoRenderer( modifier = Modifier.fillMaxSize(), diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/PortraitScreenSharingVideoRenderer.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/PortraitScreenSharingVideoRenderer.kt index 584cfde13e..03a509ecdc 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/PortraitScreenSharingVideoRenderer.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/PortraitScreenSharingVideoRenderer.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -84,7 +85,8 @@ internal fun PortraitScreenSharingVideoRenderer( Box( modifier = Modifier .fillMaxWidth() - .weight(1f), + .weight(1f) + .padding(VideoTheme.dimens.participantsGridPadding), ) { ScreenShareVideoRenderer( modifier = Modifier.fillMaxWidth(), diff --git a/stream-video-android-ui-common/src/main/res/values/dimens.xml b/stream-video-android-ui-common/src/main/res/values/dimens.xml index eda999f791..9c5af8f358 100644 --- a/stream-video-android-ui-common/src/main/res/values/dimens.xml +++ b/stream-video-android-ui-common/src/main/res/values/dimens.xml @@ -66,6 +66,7 @@ 4dp 24dp 8dp + 0dp 8dp 64dp 4dp From 30fb00b2a51a3a40f7447e0bd5ba454d13e3cd1f Mon Sep 17 00:00:00 2001 From: Jaewoong Eum Date: Wed, 30 Aug 2023 16:09:14 +0900 Subject: [PATCH 06/12] Fix using wrong resource for grids (#804) --- .../io/getstream/video/android/compose/theme/StreamDimens.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/theme/StreamDimens.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/theme/StreamDimens.kt index 480990b1e8..3fe0344714 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/theme/StreamDimens.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/theme/StreamDimens.kt @@ -208,10 +208,10 @@ public data class StreamDimens( id = R.dimen.stream_video_callParticipantLabelTextPadding, ), participantLabelTextPaddingStart = dimensionResource( - id = R.dimen.stream_video_participantsGridPadding, + id = R.dimen.stream_video_callParticipantSoundIndicatorPaddingStart, ), participantsGridPadding = dimensionResource( - id = R.dimen.stream_video_audioRoomAvatarLandscapePadding, + id = R.dimen.stream_video_participantsGridPadding, ), landscapeControlActionsButtonSize = dimensionResource( id = R.dimen.stream_video_landscapeControlActionsButtonSize, From 695032e5f5f37cceb6f283e32127f6d61232a2d3 Mon Sep 17 00:00:00 2001 From: Jaewoong Eum Date: Thu, 31 Aug 2023 15:09:16 +0900 Subject: [PATCH 07/12] Bump Kotlin to 1.9.10 and Compose compiler to 1.5.3 (#806) --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1ea4395187..1b3a7d8003 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ androidGradlePlugin = "8.1.0" spotless = "6.20.0" nexusPlugin = "1.3.0" -kotlin = "1.9.0" +kotlin = "1.9.10" kotlinSerialization = "1.5.1" kotlinSerializationConverter = "1.0.0" kotlinxCoroutines = "1.7.3" @@ -21,7 +21,7 @@ androidxDataStore = "1.0.0" googleService = "4.3.14" androidxComposeBom = "2023.08.00" -androidxComposeCompiler = "1.5.2" +androidxComposeCompiler = "1.5.3" androidxComposeTracing = "1.0.0-alpha03" androidxHiltNavigation = "1.0.0" androidxComposeNavigation = "2.7.0" From 17536d952f37f19cfa824d1eaa1d8afd2509eb58 Mon Sep 17 00:00:00 2001 From: Jaewoong Eum Date: Fri, 1 Sep 2023 09:04:39 +0900 Subject: [PATCH 08/12] AGP 8.1.1, Gradle 8.3, and dependency updates (#807) * Bump AGP 8.1.1 and other dependency versions * Bup Gradle 8.3 --- gradle/libs.versions.toml | 12 ++++++------ gradle/wrapper/gradle-wrapper.jar | Bin 61608 -> 63375 bytes gradle/wrapper/gradle-wrapper.properties | 3 ++- gradlew | 12 ++++++++---- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1b3a7d8003..861b7bcae5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,9 @@ [versions] -androidGradlePlugin = "8.1.0" -spotless = "6.20.0" +androidGradlePlugin = "8.1.1" +spotless = "6.21.0" nexusPlugin = "1.3.0" kotlin = "1.9.10" -kotlinSerialization = "1.5.1" +kotlinSerialization = "1.6.0" kotlinSerializationConverter = "1.0.0" kotlinxCoroutines = "1.7.3" @@ -27,8 +27,8 @@ androidxHiltNavigation = "1.0.0" androidxComposeNavigation = "2.7.0" composeStableMarker = "1.0.0" -coil = "2.2.2" -landscapist = "2.2.7" +coil = "2.4.0" +landscapist = "2.2.8" accompanist = "0.30.1" telephoto = "0.3.0" audioswitch = "1.1.8" @@ -50,7 +50,7 @@ streamPush = "1.1.1" androidxTest = "1.5.2" androidxTestCore = "1.5.0" androidxProfileinstaller = "1.3.1" -androidxMacroBenchmark = "1.2.0-beta03" +androidxMacroBenchmark = "1.2.0-beta05" androidxUiAutomator = "2.3.0-alpha04" androidxContraintLayout = "2.1.4" androidxEspresso = "3.5.1" diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index ccebba7710deaf9f98673a68957ea02138b60d0a..033e24c4cdf41af1ab109bc7f253b2b887023340 100644 GIT binary patch delta 21856 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 delta 20086 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/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. @@ -197,6 +197,10 @@ if "$cygwin" || "$msys" ; then done fi + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + # Collect all arguments for the java command; # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # shell script including quotes and variable substitutions, so put them in From 546ab8f78ab976206c6b66b9f4c8b5c6d372dbad Mon Sep 17 00:00:00 2001 From: Jaewoong Eum Date: Mon, 4 Sep 2023 08:28:34 +0900 Subject: [PATCH 09/12] Fix typo on livestreaming (#809) --- .../docs/Android/05-ui-cookbook/15-watching-livestream.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docusaurus/docs/Android/05-ui-cookbook/15-watching-livestream.mdx b/docusaurus/docs/Android/05-ui-cookbook/15-watching-livestream.mdx index 24eb03c4f4..1a3f4b2ac2 100644 --- a/docusaurus/docs/Android/05-ui-cookbook/15-watching-livestream.mdx +++ b/docusaurus/docs/Android/05-ui-cookbook/15-watching-livestream.mdx @@ -6,7 +6,7 @@ description: How to watch a livestream on android with Kotlin This cookbook tutorial walks you through how to build an advanced UIs for watching a livestream on Android. :::note -n this cookbook tutorial, we will assume that you already know how to join a livestream call. If you haven't familiarized yourself with the [Livestream Tutorial](../02-tutorials/03-livestream.mdx) yet, we highly recommend doing so before proceeding with this cookbook. +In this cookbook tutorial, we will assume that you already know how to join a livestream call. If you haven't familiarized yourself with the [Livestream Tutorial](../02-tutorials/03-livestream.mdx) yet, we highly recommend doing so before proceeding with this cookbook. ::: When you build a livestreaming UI, there are a few things to keep in mind: From b59515e835dffacc3f508d63f7ee3e43ed04d76b Mon Sep 17 00:00:00 2001 From: Jaewoong Eum Date: Mon, 4 Sep 2023 15:03:42 +0900 Subject: [PATCH 10/12] Implement livestream tutorials for the host and guest (#810) * Refactor livestreaming tutorials to demonstrate host and guest * invoke updateFromResponse from CallLiveStartedEvent --- .../api/stream-video-android-compose.api | 2 +- .../compose/permission/CallPermissions.kt | 8 +- .../api/stream-video-android-core.api | 1 + .../getstream/video/android/core/CallState.kt | 9 + .../tutorial-livestream/build.gradle.kts | 1 + .../android/tutorial/livestream/LiveGuest.kt | 139 +++++++++++++ .../android/tutorial/livestream/LiveHost.kt | 187 ++++++++++++++++++ .../android/tutorial/livestream/LiveMain.kt | 71 +++++++ .../tutorial/livestream/LiveNavHost.kt | 60 ++++++ .../tutorial/livestream/MainActivity.kt | 136 +------------ 10 files changed, 476 insertions(+), 138 deletions(-) create mode 100644 tutorials/tutorial-livestream/src/main/kotlin/io/getstream/video/android/tutorial/livestream/LiveGuest.kt create mode 100644 tutorials/tutorial-livestream/src/main/kotlin/io/getstream/video/android/tutorial/livestream/LiveHost.kt create mode 100644 tutorials/tutorial-livestream/src/main/kotlin/io/getstream/video/android/tutorial/livestream/LiveMain.kt create mode 100644 tutorials/tutorial-livestream/src/main/kotlin/io/getstream/video/android/tutorial/livestream/LiveNavHost.kt diff --git a/stream-video-android-compose/api/stream-video-android-compose.api b/stream-video-android-compose/api/stream-video-android-compose.api index 71d6d3e0e5..3cd74d9116 100644 --- a/stream-video-android-compose/api/stream-video-android-compose.api +++ b/stream-video-android-compose/api/stream-video-android-compose.api @@ -7,7 +7,7 @@ public final class io/getstream/video/android/compose/lifecycle/MediaPiPLifecycl } public final class io/getstream/video/android/compose/permission/CallPermissionsKt { - public static final fun LaunchCallPermissions (Lio/getstream/video/android/core/Call;Landroidx/compose/runtime/Composer;I)V + public static final fun LaunchCallPermissions (Lio/getstream/video/android/core/Call;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V public static final fun rememberCallPermissionsState (Lio/getstream/video/android/core/Call;Ljava/util/List;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)Lio/getstream/video/android/compose/permission/VideoPermissionsState; } diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/permission/CallPermissions.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/permission/CallPermissions.kt index abcd648a15..617beea7c8 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/permission/CallPermissions.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/permission/CallPermissions.kt @@ -82,7 +82,11 @@ public fun rememberCallPermissionsState( * - android.Manifest.permission.RECORD_AUDIO */ @Composable -public fun LaunchCallPermissions(call: Call) { - val callPermissionsState = rememberCallPermissionsState(call = call) +public fun LaunchCallPermissions( + call: Call, + onPermissionsResult: ((Map) -> Unit)? = null, +) { + val callPermissionsState = + rememberCallPermissionsState(call = call, onPermissionsResult = onPermissionsResult) LaunchedEffect(key1 = call) { callPermissionsState.launchPermissionRequest() } } diff --git a/stream-video-android-core/api/stream-video-android-core.api b/stream-video-android-core/api/stream-video-android-core.api index e3973462ea..ae87243396 100644 --- a/stream-video-android-core/api/stream-video-android-core.api +++ b/stream-video-android-core/api/stream-video-android-core.api @@ -111,6 +111,7 @@ public final class io/getstream/video/android/core/CallState { public final fun getDurationInDateFormat ()Lkotlinx/coroutines/flow/StateFlow; public final fun getDurationInMs ()Lkotlinx/coroutines/flow/StateFlow; public final fun getEgress ()Lkotlinx/coroutines/flow/StateFlow; + public final fun getEgressPlayListUrl ()Lkotlinx/coroutines/flow/StateFlow; public final fun getEndedAt ()Lkotlinx/coroutines/flow/StateFlow; public final fun getEndedByUser ()Lkotlinx/coroutines/flow/StateFlow; public final fun getErrors ()Lkotlinx/coroutines/flow/StateFlow; diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt index d26e9a7bdc..c8991c7830 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt @@ -62,6 +62,7 @@ import org.openapitools.client.models.CallAcceptedEvent import org.openapitools.client.models.CallCreatedEvent import org.openapitools.client.models.CallEndedEvent import org.openapitools.client.models.CallIngressResponse +import org.openapitools.client.models.CallLiveStartedEvent import org.openapitools.client.models.CallMemberAddedEvent import org.openapitools.client.models.CallMemberRemovedEvent import org.openapitools.client.models.CallMemberUpdatedEvent @@ -408,6 +409,10 @@ public class CallState( private val _egress: MutableStateFlow = MutableStateFlow(null) val egress: StateFlow = _egress + public val egressPlayListUrl: StateFlow = egress.mapState { + it?.hls?.playlistUrl + } + private val _broadcasting: MutableStateFlow = MutableStateFlow(false) /** if the call is being broadcasted to HLS */ @@ -624,6 +629,10 @@ public class CallState( _recording.value = false } + is CallLiveStartedEvent -> { + updateFromResponse(event.call) + } + is AudioLevelChangedEvent -> { event.levels.forEach { entry -> val participant = getOrCreateParticipant(entry.key, entry.value.userId) diff --git a/tutorials/tutorial-livestream/build.gradle.kts b/tutorials/tutorial-livestream/build.gradle.kts index 79c44beb0c..e8fb84145a 100644 --- a/tutorials/tutorial-livestream/build.gradle.kts +++ b/tutorials/tutorial-livestream/build.gradle.kts @@ -48,4 +48,5 @@ dependencies { implementation(libs.androidx.compose.runtime) implementation(libs.androidx.compose.foundation) implementation(libs.androidx.compose.material) + implementation(libs.androidx.compose.navigation) } \ No newline at end of file diff --git a/tutorials/tutorial-livestream/src/main/kotlin/io/getstream/video/android/tutorial/livestream/LiveGuest.kt b/tutorials/tutorial-livestream/src/main/kotlin/io/getstream/video/android/tutorial/livestream/LiveGuest.kt new file mode 100644 index 0000000000..b8f4a2d153 --- /dev/null +++ b/tutorials/tutorial-livestream/src/main/kotlin/io/getstream/video/android/tutorial/livestream/LiveGuest.kt @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-video-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.video.android.tutorial.livestream + +import android.util.Log +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.core.Call +import io.getstream.video.android.core.GEO +import io.getstream.video.android.core.StreamVideoBuilder +import io.getstream.video.android.model.User + +@Composable +fun LiveAudience() { + val context = LocalContext.current + + var call: Call? by remember { mutableStateOf(null) } + + LaunchedEffect(key1 = Unit) { + val userToken = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiQWRtaXJhbF9BY2tiYXIiLCJpc3MiOiJwcm9udG8iLCJzdWIiOiJ1c2VyL0FkbWlyYWxfQWNrYmFyIiwiaWF0IjoxNjkzNzk0NTc4LCJleHAiOjE2OTQzOTkzODN9.7uYF4xB1zUrQ1GIpsoICoU5G0DpXq_5_IDyohz6p3VU" + val userId = "Admiral_Ackbar" + val callId = "szua8Iy5iMX2" + + // step1 - create a user. + val user = User( + id = userId, // any string + name = "Tutorial", // name and image are used in the UI + role = "admin", + ) + + // step2 - initialize StreamVideo. For a production app we recommend adding the client to your Application class or di module. + val client = StreamVideoBuilder( + context = context, + apiKey = "mmhfdzb5evj2", // demo API key + geo = GEO.GlobalEdgeNetwork, + user = user, + token = userToken, + ensureSingleInstance = false, + ).build() + + // step3 - join a call, which type is `default` and id is `123`. + call = client.call("livestream", callId) + + // join the call + val result = call?.join() + result?.onError { + Toast.makeText(context, "uh oh $it", Toast.LENGTH_SHORT).show() + } + } + + if (call != null) { + LiveGuestContent(call!!) + } +} + +@Composable +private fun LiveGuestContent(call: Call) { + val participants by call.state.participants.collectAsState() + val totalParticipants by call.state.totalParticipants.collectAsState() + val backstage by call.state.backstage.collectAsState() + val duration by call.state.duration.collectAsState() + + LaunchedEffect(key1 = participants) { + Log.e("Test", "participants: $participants") + } + + Column( + modifier = Modifier + .fillMaxSize() + .background(VideoTheme.colors.appBackground) + .padding(6.dp), + ) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(6.dp), + ) { + if (backstage) { + Text( + modifier = Modifier.align(Alignment.Center), + text = "Waiting for live host", + color = VideoTheme.colors.textHighEmphasis, + ) + } else { + Text( + modifier = Modifier + .align(Alignment.CenterEnd) + .background( + color = VideoTheme.colors.primaryAccent, + shape = RoundedCornerShape(6.dp), + ) + .padding(horizontal = 12.dp, vertical = 4.dp), + text = "Live $totalParticipants", + color = Color.White, + ) + + Text( + modifier = Modifier.align(Alignment.Center), + text = "Live for $duration", + color = VideoTheme.colors.textHighEmphasis, + ) + } + } + } +} diff --git a/tutorials/tutorial-livestream/src/main/kotlin/io/getstream/video/android/tutorial/livestream/LiveHost.kt b/tutorials/tutorial-livestream/src/main/kotlin/io/getstream/video/android/tutorial/livestream/LiveHost.kt new file mode 100644 index 0000000000..8c3d59dc5e --- /dev/null +++ b/tutorials/tutorial-livestream/src/main/kotlin/io/getstream/video/android/tutorial/livestream/LiveHost.kt @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-video-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.video.android.tutorial.livestream + +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import io.getstream.video.android.compose.permission.LaunchCallPermissions +import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.ui.components.video.VideoRenderer +import io.getstream.video.android.core.Call +import io.getstream.video.android.core.GEO +import io.getstream.video.android.core.RealtimeConnection +import io.getstream.video.android.core.StreamVideoBuilder +import io.getstream.video.android.model.User +import kotlinx.coroutines.launch + +@Composable +fun LiveHost() { + val context = LocalContext.current + + var call: Call? by remember { mutableStateOf(null) } + + LaunchedEffect(key1 = Unit) { + val userToken = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiQWRtaXJhbF9BY2tiYXIiLCJpc3MiOiJwcm9udG8iLCJzdWIiOiJ1c2VyL0FkbWlyYWxfQWNrYmFyIiwiaWF0IjoxNjkzNzk0NTc4LCJleHAiOjE2OTQzOTkzODN9.7uYF4xB1zUrQ1GIpsoICoU5G0DpXq_5_IDyohz6p3VU" + val userId = "Admiral_Ackbar" + val callId = "szua8Iy5iMX2" + + // step1 - create a user. + val user = User( + id = userId, // any string + name = "Tutorial", // name and image are used in the UI + role = "admin", + ) + + // step2 - initialize StreamVideo. For a production app we recommend adding the client to your Application class or di module. + val client = StreamVideoBuilder( + context = context, + apiKey = "mmhfdzb5evj2", // demo API key + geo = GEO.GlobalEdgeNetwork, + user = user, + token = userToken, + ensureSingleInstance = false, + ).build() + + // step3 - join a call, which type is `default` and id is `123`. + call = client.call("livestream", callId) + + // join the call + val result = call?.join(create = true) + result?.onError { + Toast.makeText(context, "uh oh $it", Toast.LENGTH_SHORT).show() + } + } + + if (call != null) { + LiveHostContent(call!!) + } +} + +@Composable +private fun LiveHostContent(call: Call) { + LaunchCallPermissions(call = call) + + val connection by call.state.connection.collectAsState() + val totalParticipants by call.state.totalParticipants.collectAsState() + val backstage by call.state.backstage.collectAsState() + val localParticipant by call.state.localParticipant.collectAsState() + val video = localParticipant?.video?.collectAsState()?.value + val duration by call.state.duration.collectAsState() + val scope = rememberCoroutineScope() + + Scaffold( + modifier = Modifier + .fillMaxSize() + .background(VideoTheme.colors.appBackground) + .padding(6.dp), + contentColor = VideoTheme.colors.appBackground, + backgroundColor = VideoTheme.colors.appBackground, + topBar = { + if (connection == RealtimeConnection.Connected) { + if (!backstage) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(6.dp), + ) { + Text( + modifier = Modifier + .align(Alignment.CenterEnd) + .background( + color = VideoTheme.colors.primaryAccent, + shape = RoundedCornerShape(6.dp), + ) + .padding(horizontal = 12.dp, vertical = 4.dp), + text = "Live $totalParticipants", + color = Color.White, + ) + + Text( + modifier = Modifier.align(Alignment.Center), + text = "Live for $duration", + color = VideoTheme.colors.textHighEmphasis, + ) + } + } else { + Text( + text = "Backstage", + color = VideoTheme.colors.textHighEmphasis, + ) + } + } else if (connection is RealtimeConnection.Failed) { + Text( + text = "Connection failed", + color = VideoTheme.colors.textHighEmphasis, + ) + } + }, + bottomBar = { + Button( + colors = ButtonDefaults.buttonColors( + contentColor = VideoTheme.colors.primaryAccent, + backgroundColor = VideoTheme.colors.primaryAccent, + ), + onClick = { + scope.launch { + if (backstage) call.goLive() else call.stopLive() + } + }, + ) { + Text( + text = if (backstage) "Start Broadcast" else "Stop Broadcast", + color = Color.White, + ) + } + }, + ) { + VideoRenderer( + modifier = Modifier + .fillMaxSize() + .padding(it) + .clip(RoundedCornerShape(6.dp)), + call = call, + video = video, + videoFallbackContent = { + Text(text = "Video rendering failed") + }, + ) + } +} diff --git a/tutorials/tutorial-livestream/src/main/kotlin/io/getstream/video/android/tutorial/livestream/LiveMain.kt b/tutorials/tutorial-livestream/src/main/kotlin/io/getstream/video/android/tutorial/livestream/LiveMain.kt new file mode 100644 index 0000000000..9429b3721b --- /dev/null +++ b/tutorials/tutorial-livestream/src/main/kotlin/io/getstream/video/android/tutorial/livestream/LiveMain.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-video-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.video.android.tutorial.livestream + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import io.getstream.video.android.compose.theme.VideoTheme + +@Composable +fun LiveMain( + navController: NavHostController, +) { + Box( + modifier = Modifier + .fillMaxSize() + .background(VideoTheme.colors.appBackground), + ) { + Column { + Button( + colors = ButtonDefaults.buttonColors( + contentColor = VideoTheme.colors.primaryAccent, + backgroundColor = VideoTheme.colors.primaryAccent, + ), + onClick = { + navController.navigate(LiveScreens.Host.destination) + }, + ) { + Text(text = "host", color = VideoTheme.colors.textHighEmphasis) + } + + Spacer(modifier = Modifier.height(20.dp)) + + Button( + colors = ButtonDefaults.buttonColors( + contentColor = VideoTheme.colors.primaryAccent, + backgroundColor = VideoTheme.colors.primaryAccent, + ), + onClick = { + navController.navigate(LiveScreens.Guest.destination) + }, + ) { + Text(text = "guest", color = VideoTheme.colors.textHighEmphasis) + } + } + } +} diff --git a/tutorials/tutorial-livestream/src/main/kotlin/io/getstream/video/android/tutorial/livestream/LiveNavHost.kt b/tutorials/tutorial-livestream/src/main/kotlin/io/getstream/video/android/tutorial/livestream/LiveNavHost.kt new file mode 100644 index 0000000000..c13c02afd0 --- /dev/null +++ b/tutorials/tutorial-livestream/src/main/kotlin/io/getstream/video/android/tutorial/livestream/LiveNavHost.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-video-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.video.android.tutorial.livestream + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import io.getstream.video.android.compose.theme.VideoTheme + +@Composable +fun LiveNavHost( + modifier: Modifier = Modifier, + navController: NavHostController = rememberNavController(), + startDestination: String = LiveScreens.Main.destination, +) { + NavHost( + modifier = modifier + .fillMaxSize() + .background(VideoTheme.colors.appBackground), + navController = navController, + startDestination = startDestination, + ) { + composable(LiveScreens.Main.destination) { + LiveMain(navController = navController) + } + + composable(LiveScreens.Host.destination) { + LiveHost() + } + + composable(LiveScreens.Guest.destination) { + LiveAudience() + } + } +} + +enum class LiveScreens(val destination: String) { + Main("main"), + Host("host"), + Guest("audience"), +} diff --git a/tutorials/tutorial-livestream/src/main/kotlin/io/getstream/video/android/tutorial/livestream/MainActivity.kt b/tutorials/tutorial-livestream/src/main/kotlin/io/getstream/video/android/tutorial/livestream/MainActivity.kt index 6cbf9f5cdd..5d13defc41 100644 --- a/tutorials/tutorial-livestream/src/main/kotlin/io/getstream/video/android/tutorial/livestream/MainActivity.kt +++ b/tutorials/tutorial-livestream/src/main/kotlin/io/getstream/video/android/tutorial/livestream/MainActivity.kt @@ -17,151 +17,17 @@ package io.getstream.video.android.tutorial.livestream import android.os.Bundle -import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Scaffold -import androidx.compose.material.Text -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import androidx.lifecycle.lifecycleScope -import io.getstream.video.android.compose.permission.LaunchCallPermissions import io.getstream.video.android.compose.theme.VideoTheme -import io.getstream.video.android.compose.ui.components.video.VideoRenderer -import io.getstream.video.android.core.GEO -import io.getstream.video.android.core.RealtimeConnection -import io.getstream.video.android.core.StreamVideoBuilder -import io.getstream.video.android.model.User -import kotlinx.coroutines.launch class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val userToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiQnJha2lzcyIsImlzcyI6InByb250byIsInN1YiI6InVzZXIvQnJha2lzcyIsImlhdCI6MTY5MTE3MDE1OSwiZXhwIjoxNjkxNzc0OTY0fQ.VVEvnlQK7s-MY3FZ_gnq5K_9-NywidhWZjJiGuARCmQ" - val userId = "Brakiss" - val callId = "gu0aLbmH9zRf" - - // step1 - create a user. - val user = User( - id = userId, // any string - name = "Tutorial", // name and image are used in the UI - role = "admin", - ) - - // step2 - initialize StreamVideo. For a production app we recommend adding the client to your Application class or di module. - val client = StreamVideoBuilder( - context = applicationContext, - apiKey = "hd8szvscpxvd", // demo API key - geo = GEO.GlobalEdgeNetwork, - user = user, - token = userToken, - ).build() - - // step3 - join a call, which type is `default` and id is `123`. - val call = client.call("livestream", callId) - lifecycleScope.launch { - // join the call - val result = call.join(create = true) - result.onError { - Toast.makeText(applicationContext, "uh oh $it", Toast.LENGTH_SHORT).show() - } - } - setContent { - // request the Android runtime permissions for the camera and microphone - LaunchCallPermissions(call = call) - - // step4 - apply VideoTheme - VideoTheme { - val connection by call.state.connection.collectAsState() - val totalParticipants by call.state.totalParticipants.collectAsState() - val backstage by call.state.backstage.collectAsState() - val localParticipant by call.state.localParticipant.collectAsState() - val video = localParticipant?.video?.collectAsState()?.value - val duration by call.state.duration.collectAsState() - - androidx.compose.material.Scaffold( - modifier = Modifier - .fillMaxSize() - .background(VideoTheme.colors.appBackground) - .padding(6.dp), - contentColor = VideoTheme.colors.appBackground, - backgroundColor = VideoTheme.colors.appBackground, - topBar = { - if (connection == RealtimeConnection.Connected) { - if (!backstage) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(6.dp), - ) { - Text( - modifier = Modifier - .align(Alignment.CenterEnd) - .background( - color = VideoTheme.colors.primaryAccent, - shape = RoundedCornerShape(6.dp), - ) - .padding(horizontal = 12.dp, vertical = 4.dp), - text = "Live $totalParticipants", - color = Color.White, - ) - - Text( - modifier = Modifier.align(Alignment.Center), - text = "Live for $duration", - color = VideoTheme.colors.textHighEmphasis, - ) - } - } - } - }, - bottomBar = { - androidx.compose.material.Button( - colors = ButtonDefaults.buttonColors( - contentColor = VideoTheme.colors.primaryAccent, - backgroundColor = VideoTheme.colors.primaryAccent, - ), - onClick = { - lifecycleScope.launch { - if (backstage) call.goLive() else call.stopLive() - } - }, - ) { - Text( - text = if (backstage) "Go Live" else "Stop Broadcast", - color = Color.White, - ) - } - }, - ) { - VideoRenderer( - modifier = Modifier - .fillMaxSize() - .padding(it) - .clip(RoundedCornerShape(6.dp)), - call = call, - video = video, - videoFallbackContent = { - Text(text = "Video rendering failed") - }, - ) - } - } + VideoTheme { LiveNavHost() } } } } From 784784f4786b0ae671745b9c29b78d93ba0833da Mon Sep 17 00:00:00 2001 From: Jaewoong Eum Date: Mon, 4 Sep 2023 15:45:55 +0900 Subject: [PATCH 11/12] Migrate object with sealed into data object (#811) --- .../api/stream-video-android-core.api | 88 +++++++++++++++++++ .../getstream/video/android/core/CallState.kt | 10 +-- .../video/android/core/ClientState.kt | 18 ++-- .../video/android/core/MediaManager.kt | 4 +- .../android/core/call/state/CallAction.kt | 16 ++-- .../android/core/errors/DisconnectCause.kt | 4 +- .../video/android/core/model/CallStatus.kt | 4 +- .../video/android/core/model/Reaction.kt | 4 +- .../api/stream-video-android-model.api | 9 ++ .../io/getstream/video/android/model/User.kt | 6 +- .../xml/imageloading/StreamImageLoader.kt | 4 +- 11 files changed, 132 insertions(+), 35 deletions(-) diff --git a/stream-video-android-core/api/stream-video-android-core.api b/stream-video-android-core/api/stream-video-android-core.api index ae87243396..ddde26682f 100644 --- a/stream-video-android-core/api/stream-video-android-core.api +++ b/stream-video-android-core/api/stream-video-android-core.api @@ -204,10 +204,16 @@ public abstract class io/getstream/video/android/core/CameraDirection { public final class io/getstream/video/android/core/CameraDirection$Back : io/getstream/video/android/core/CameraDirection { public static final field INSTANCE Lio/getstream/video/android/core/CameraDirection$Back; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class io/getstream/video/android/core/CameraDirection$Front : io/getstream/video/android/core/CameraDirection { public static final field INSTANCE Lio/getstream/video/android/core/CameraDirection$Front; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class io/getstream/video/android/core/CameraManager { @@ -261,10 +267,16 @@ public abstract interface class io/getstream/video/android/core/ConnectionState public final class io/getstream/video/android/core/ConnectionState$Connected : io/getstream/video/android/core/ConnectionState { public static final field INSTANCE Lio/getstream/video/android/core/ConnectionState$Connected; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class io/getstream/video/android/core/ConnectionState$Disconnected : io/getstream/video/android/core/ConnectionState { public static final field INSTANCE Lio/getstream/video/android/core/ConnectionState$Disconnected; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class io/getstream/video/android/core/ConnectionState$Failed : io/getstream/video/android/core/ConnectionState { @@ -273,14 +285,23 @@ public final class io/getstream/video/android/core/ConnectionState$Failed : io/g public final class io/getstream/video/android/core/ConnectionState$Loading : io/getstream/video/android/core/ConnectionState { public static final field INSTANCE Lio/getstream/video/android/core/ConnectionState$Loading; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class io/getstream/video/android/core/ConnectionState$PreConnect : io/getstream/video/android/core/ConnectionState { public static final field INSTANCE Lio/getstream/video/android/core/ConnectionState$PreConnect; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class io/getstream/video/android/core/ConnectionState$Reconnecting : io/getstream/video/android/core/ConnectionState { public static final field INSTANCE Lio/getstream/video/android/core/ConnectionState$Reconnecting; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class io/getstream/video/android/core/CreateCallOptions { @@ -540,10 +561,16 @@ public abstract interface class io/getstream/video/android/core/RealtimeConnecti public final class io/getstream/video/android/core/RealtimeConnection$Connected : io/getstream/video/android/core/RealtimeConnection { public static final field INSTANCE Lio/getstream/video/android/core/RealtimeConnection$Connected; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class io/getstream/video/android/core/RealtimeConnection$Disconnected : io/getstream/video/android/core/RealtimeConnection { public static final field INSTANCE Lio/getstream/video/android/core/RealtimeConnection$Disconnected; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class io/getstream/video/android/core/RealtimeConnection$Failed : io/getstream/video/android/core/RealtimeConnection { @@ -559,6 +586,9 @@ public final class io/getstream/video/android/core/RealtimeConnection$Failed : i public final class io/getstream/video/android/core/RealtimeConnection$InProgress : io/getstream/video/android/core/RealtimeConnection { public static final field INSTANCE Lio/getstream/video/android/core/RealtimeConnection$InProgress; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class io/getstream/video/android/core/RealtimeConnection$Joined : io/getstream/video/android/core/RealtimeConnection { @@ -574,10 +604,16 @@ public final class io/getstream/video/android/core/RealtimeConnection$Joined : i public final class io/getstream/video/android/core/RealtimeConnection$PreJoin : io/getstream/video/android/core/RealtimeConnection { public static final field INSTANCE Lio/getstream/video/android/core/RealtimeConnection$PreJoin; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class io/getstream/video/android/core/RealtimeConnection$Reconnecting : io/getstream/video/android/core/RealtimeConnection { public static final field INSTANCE Lio/getstream/video/android/core/RealtimeConnection$Reconnecting; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public abstract interface class io/getstream/video/android/core/RingingState { @@ -585,10 +621,16 @@ public abstract interface class io/getstream/video/android/core/RingingState { public final class io/getstream/video/android/core/RingingState$Active : io/getstream/video/android/core/RingingState { public static final field INSTANCE Lio/getstream/video/android/core/RingingState$Active; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class io/getstream/video/android/core/RingingState$Idle : io/getstream/video/android/core/RingingState { public static final field INSTANCE Lio/getstream/video/android/core/RingingState$Idle; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class io/getstream/video/android/core/RingingState$Incoming : io/getstream/video/android/core/RingingState { @@ -609,10 +651,16 @@ public final class io/getstream/video/android/core/RingingState$Outgoing : io/ge public final class io/getstream/video/android/core/RingingState$RejectedByAll : io/getstream/video/android/core/RingingState { public static final field INSTANCE Lio/getstream/video/android/core/RingingState$RejectedByAll; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class io/getstream/video/android/core/RingingState$TimeoutNoAnswer : io/getstream/video/android/core/RingingState { public static final field INSTANCE Lio/getstream/video/android/core/RingingState$TimeoutNoAnswer; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class io/getstream/video/android/core/SpeakerManager { @@ -904,6 +952,9 @@ public final class io/getstream/video/android/core/call/signal/socket/RTCEventMa public final class io/getstream/video/android/core/call/state/AcceptCall : io/getstream/video/android/core/call/state/CallAction { public static final field INSTANCE Lio/getstream/video/android/core/call/state/AcceptCall; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public abstract interface class io/getstream/video/android/core/call/state/CallAction { @@ -911,10 +962,16 @@ public abstract interface class io/getstream/video/android/core/call/state/CallA public final class io/getstream/video/android/core/call/state/CancelCall : io/getstream/video/android/core/call/state/CallAction { public static final field INSTANCE Lio/getstream/video/android/core/call/state/CancelCall; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class io/getstream/video/android/core/call/state/ChatDialog : io/getstream/video/android/core/call/state/CallAction { public static final field INSTANCE Lio/getstream/video/android/core/call/state/ChatDialog; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public class io/getstream/video/android/core/call/state/CustomAction : io/getstream/video/android/core/call/state/CallAction { @@ -926,10 +983,16 @@ public class io/getstream/video/android/core/call/state/CustomAction : io/getstr public final class io/getstream/video/android/core/call/state/DeclineCall : io/getstream/video/android/core/call/state/CallAction { public static final field INSTANCE Lio/getstream/video/android/core/call/state/DeclineCall; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class io/getstream/video/android/core/call/state/FlipCamera : io/getstream/video/android/core/call/state/CallAction { public static final field INSTANCE Lio/getstream/video/android/core/call/state/FlipCamera; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class io/getstream/video/android/core/call/state/InviteUsersToCall : io/getstream/video/android/core/call/state/CallAction { @@ -945,10 +1008,16 @@ public final class io/getstream/video/android/core/call/state/InviteUsersToCall public final class io/getstream/video/android/core/call/state/LeaveCall : io/getstream/video/android/core/call/state/CallAction { public static final field INSTANCE Lio/getstream/video/android/core/call/state/LeaveCall; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class io/getstream/video/android/core/call/state/Reaction : io/getstream/video/android/core/call/state/CallAction { public static final field INSTANCE Lio/getstream/video/android/core/call/state/Reaction; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class io/getstream/video/android/core/call/state/SelectAudioDevice : io/getstream/video/android/core/call/state/CallAction { @@ -964,6 +1033,9 @@ public final class io/getstream/video/android/core/call/state/SelectAudioDevice public final class io/getstream/video/android/core/call/state/Settings : io/getstream/video/android/core/call/state/CallAction { public static final field INSTANCE Lio/getstream/video/android/core/call/state/Settings; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class io/getstream/video/android/core/call/state/ShowCallParticipantInfo : io/getstream/video/android/core/call/state/CallAction { @@ -1039,6 +1111,8 @@ public abstract class io/getstream/video/android/core/errors/DisconnectCause { public final class io/getstream/video/android/core/errors/DisconnectCause$ConnectionReleased : io/getstream/video/android/core/errors/DisconnectCause { public static final field INSTANCE Lio/getstream/video/android/core/errors/DisconnectCause$ConnectionReleased; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -1049,6 +1123,8 @@ public final class io/getstream/video/android/core/errors/DisconnectCause$Error public final class io/getstream/video/android/core/errors/DisconnectCause$NetworkNotAvailable : io/getstream/video/android/core/errors/DisconnectCause { public static final field INSTANCE Lio/getstream/video/android/core/errors/DisconnectCause$NetworkNotAvailable; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -1765,10 +1841,16 @@ public final class io/getstream/video/android/core/model/CallStatus$Calling : io public final class io/getstream/video/android/core/model/CallStatus$Incoming : io/getstream/video/android/core/model/CallStatus { public static final field INSTANCE Lio/getstream/video/android/core/model/CallStatus$Incoming; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class io/getstream/video/android/core/model/CallStatus$Outgoing : io/getstream/video/android/core/model/CallStatus { public static final field INSTANCE Lio/getstream/video/android/core/model/CallStatus$Outgoing; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class io/getstream/video/android/core/model/CallUser : java/io/Serializable { @@ -2096,10 +2178,16 @@ public abstract interface class io/getstream/video/android/core/model/ReactionSt public final class io/getstream/video/android/core/model/ReactionState$Nothing : io/getstream/video/android/core/model/ReactionState { public static final field INSTANCE Lio/getstream/video/android/core/model/ReactionState$Nothing; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class io/getstream/video/android/core/model/ReactionState$Running : io/getstream/video/android/core/model/ReactionState { public static final field INSTANCE Lio/getstream/video/android/core/model/ReactionState$Running; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class io/getstream/video/android/core/model/ScreenSharingSession { diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt index c8991c7830..73ccf1e60d 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt @@ -120,12 +120,12 @@ public sealed interface RealtimeConnection { /** * We start out in the PreJoin state. This is before call.join is called */ - public object PreJoin : RealtimeConnection + public data object PreJoin : RealtimeConnection /** * Join is in progress */ - public object InProgress : RealtimeConnection + public data object InProgress : RealtimeConnection /** * We set the state to Joined as soon as the call state is available @@ -136,7 +136,7 @@ public sealed interface RealtimeConnection { /** * True when the peer connections are ready */ - public object Connected : RealtimeConnection // connected to RTC, able to receive and send video + public data object Connected : RealtimeConnection // connected to RTC, able to receive and send video /** * Reconnecting is true whenever Rtc isn't available and trying to recover @@ -144,9 +144,9 @@ public sealed interface RealtimeConnection { * If the publisher peer connection breaks we'll reconnect * Also if the network provider from the OS says that internet is down we'll set it to reconnecting */ - public object Reconnecting : RealtimeConnection // reconnecting to recover from temporary issues + public data object Reconnecting : RealtimeConnection // reconnecting to recover from temporary issues public data class Failed(val error: Any) : RealtimeConnection // permanent failure - public object Disconnected : RealtimeConnection // normal disconnect by the app + public data object Disconnected : RealtimeConnection // normal disconnect by the app } /** diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ClientState.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ClientState.kt index 66af881e3b..58b523aed7 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ClientState.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ClientState.kt @@ -26,21 +26,21 @@ import org.openapitools.client.models.ConnectedEvent import org.openapitools.client.models.VideoEvent public sealed interface ConnectionState { - public object PreConnect : ConnectionState - public object Loading : ConnectionState - public object Connected : ConnectionState - public object Reconnecting : ConnectionState - public object Disconnected : ConnectionState + public data object PreConnect : ConnectionState + public data object Loading : ConnectionState + public data object Connected : ConnectionState + public data object Reconnecting : ConnectionState + public data object Disconnected : ConnectionState public class Failed(error: Error) : ConnectionState } public sealed interface RingingState { - public object Idle : RingingState + public data object Idle : RingingState public data class Incoming(val acceptedByMe: Boolean) : RingingState public class Outgoing(val acceptedByCallee: Boolean) : RingingState - public object Active : RingingState - public object RejectedByAll : RingingState - public object TimeoutNoAnswer : RingingState + public data object Active : RingingState + public data object RejectedByAll : RingingState + public data object TimeoutNoAnswer : RingingState } class ClientState(client: StreamVideo) { diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.kt index 1ac2f2ce4a..64cb5ab001 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.kt @@ -290,8 +290,8 @@ class MicrophoneManager( } public sealed class CameraDirection { - public object Front : CameraDirection() - public object Back : CameraDirection() + public data object Front : CameraDirection() + public data object Back : CameraDirection() } /** diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/state/CallAction.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/state/CallAction.kt index 20871d35e5..a6d8571acb 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/state/CallAction.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/state/CallAction.kt @@ -55,42 +55,42 @@ public data class ToggleMicrophone( /** * Action to flip the active camera. */ -public object FlipCamera : CallAction +public data object FlipCamera : CallAction /** * Action to accept a call in Incoming Call state. */ -public object AcceptCall : CallAction +public data object AcceptCall : CallAction /** * Action used to cancel an outgoing call. */ -public object CancelCall : CallAction +public data object CancelCall : CallAction /** * Action to decline an oncoming call. */ -public object DeclineCall : CallAction +public data object DeclineCall : CallAction /** * Action to leave the call. */ -public object LeaveCall : CallAction +public data object LeaveCall : CallAction /** * Action to show a chat dialog. */ -public object ChatDialog : CallAction +public data object ChatDialog : CallAction /** * Action to show a settings. */ -public object Settings : CallAction +public data object Settings : CallAction /** * Action to show a reaction popup. */ -public object Reaction : CallAction +public data object Reaction : CallAction /** * Action to invite other users to a call. diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/errors/DisconnectCause.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/errors/DisconnectCause.kt index 9ee343104c..46a6c69755 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/errors/DisconnectCause.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/errors/DisconnectCause.kt @@ -24,7 +24,7 @@ public sealed class DisconnectCause { /** * Happens when networks is not available anymore. */ - public object NetworkNotAvailable : DisconnectCause() { override fun toString(): String = "NetworkNotAvailable" } + public data object NetworkNotAvailable : DisconnectCause() { override fun toString(): String = "NetworkNotAvailable" } /** * Happens when some non critical error occurs. @@ -45,5 +45,5 @@ public sealed class DisconnectCause { /** * Happens when disconnection has been done intentionally. E.g. we release connection when app went to background. */ - public object ConnectionReleased : DisconnectCause() { override fun toString(): String = "ConnectionReleased" } + public data object ConnectionReleased : DisconnectCause() { override fun toString(): String = "ConnectionReleased" } } diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/model/CallStatus.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/model/CallStatus.kt index 160e886977..7e3341c007 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/model/CallStatus.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/model/CallStatus.kt @@ -22,10 +22,10 @@ import androidx.compose.runtime.Stable public sealed interface CallStatus { @Stable - public object Incoming : CallStatus + public data object Incoming : CallStatus @Stable - public object Outgoing : CallStatus + public data object Outgoing : CallStatus @Stable public data class Calling(public val duration: String) : CallStatus diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/model/Reaction.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/model/Reaction.kt index eac3579eb5..f663860b83 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/model/Reaction.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/model/Reaction.kt @@ -31,8 +31,8 @@ public data class Reaction( public sealed interface ReactionState { @Stable - public object Nothing : ReactionState + public data object Nothing : ReactionState @Stable - public object Running : ReactionState + public data object Running : ReactionState } diff --git a/stream-video-android-model/api/stream-video-android-model.api b/stream-video-android-model/api/stream-video-android-model.api index ba64207c75..8b6cb71ee4 100644 --- a/stream-video-android-model/api/stream-video-android-model.api +++ b/stream-video-android-model/api/stream-video-android-model.api @@ -198,12 +198,18 @@ public abstract class io/getstream/video/android/model/UserType { public final class io/getstream/video/android/model/UserType$Anonymous : io/getstream/video/android/model/UserType { public static final field INSTANCE Lio/getstream/video/android/model/UserType$Anonymous; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I public final fun serializer ()Lkotlinx/serialization/KSerializer; + public fun toString ()Ljava/lang/String; } public final class io/getstream/video/android/model/UserType$Authenticated : io/getstream/video/android/model/UserType { public static final field INSTANCE Lio/getstream/video/android/model/UserType$Authenticated; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I public final fun serializer ()Lkotlinx/serialization/KSerializer; + public fun toString ()Ljava/lang/String; } public final class io/getstream/video/android/model/UserType$Companion { @@ -212,7 +218,10 @@ public final class io/getstream/video/android/model/UserType$Companion { public final class io/getstream/video/android/model/UserType$Guest : io/getstream/video/android/model/UserType { public static final field INSTANCE Lio/getstream/video/android/model/UserType$Guest; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I public final fun serializer ()Lkotlinx/serialization/KSerializer; + public fun toString ()Ljava/lang/String; } public final class io/getstream/video/android/model/mapper/StreamCallCidMapperKt { diff --git a/stream-video-android-model/src/main/kotlin/io/getstream/video/android/model/User.kt b/stream-video-android-model/src/main/kotlin/io/getstream/video/android/model/User.kt index 96fe15126f..62d8c03b80 100644 --- a/stream-video-android-model/src/main/kotlin/io/getstream/video/android/model/User.kt +++ b/stream-video-android-model/src/main/kotlin/io/getstream/video/android/model/User.kt @@ -32,15 +32,15 @@ import org.threeten.bp.format.DateTimeFormatter public sealed class UserType { /** A user that's authenticated in your system */ @Serializable - public object Authenticated : UserType() + public data object Authenticated : UserType() /** A temporary guest user, that can have an image, name etc */ @Serializable - public object Guest : UserType() + public data object Guest : UserType() /** Not authentication, anonymous user. Commonly used for audio rooms and livestreams */ @Serializable - public object Anonymous : UserType() + public data object Anonymous : UserType() } @Serializer(forClass = OffsetDateTime::class) diff --git a/stream-video-android-xml/src/main/kotlin/io/getstream/video/android/xml/imageloading/StreamImageLoader.kt b/stream-video-android-xml/src/main/kotlin/io/getstream/video/android/xml/imageloading/StreamImageLoader.kt index 36fa6ae6f1..e9894036a1 100644 --- a/stream-video-android-xml/src/main/kotlin/io/getstream/video/android/xml/imageloading/StreamImageLoader.kt +++ b/stream-video-android-xml/src/main/kotlin/io/getstream/video/android/xml/imageloading/StreamImageLoader.kt @@ -80,8 +80,8 @@ internal sealed interface StreamImageLoader { ): Bitmap? public sealed class ImageTransformation { - public object None : ImageTransformation() - public object Circle : ImageTransformation() + public data object None : ImageTransformation() + public data object Circle : ImageTransformation() public class RoundedCorners(@Px public val radius: Float) : ImageTransformation() } } From 80da7376d9d93f5b7fd2c53ea3bd443894fbd07f Mon Sep 17 00:00:00 2001 From: Jaewoong Eum Date: Tue, 5 Sep 2023 08:23:13 +0900 Subject: [PATCH 12/12] Update todo list (#812) --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 94fd2de2bc..019372b51d 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ Video roadmap and changelog is available [here](https://github.com/GetStream/pro ### 0.3.0 milestone -- [ ] Finish usability testing with design team on chat integration (Jaewoong) +- [X] Finish usability testing with design team on chat integration (Jaewoong) - [X] Pagination on query members & query call endpoints (Daniel) - [X] Livestream tutorial (depends on RTMP support) (Thierry) - [X] local version of audioLevel(s) for lower latency audio visualizations(Daniel) @@ -115,6 +115,7 @@ Video roadmap and changelog is available [here](https://github.com/GetStream/pro ### 0.4.0 milestone +- [ ] Complete Livestreaming APIs and Tutorials for hosting & watching - [ ] Android SDK development.md cleanup (Daniel) - [ ] Upgrade to more recent versions of webrtc (Kanat) - [ ] Picture of the video stream at highest resolution