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 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: 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/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 } 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..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.0" -kotlinSerialization = "1.5.1" +kotlin = "1.9.10" +kotlinSerialization = "1.6.0" kotlinSerializationConverter = "1.0.0" kotlinxCoroutines = "1.7.3" @@ -21,14 +21,14 @@ 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" 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" @@ -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" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index ccebba7710..033e24c4cd 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9b0a13f0fb..d11cdd907d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 79a61d421c..fcb6fca147 100755 --- a/gradlew +++ b/gradlew @@ -85,9 +85,6 @@ done APP_BASE_NAME=${0##*/} APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit -# 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"' - # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,10 +130,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/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 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..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; } @@ -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/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-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..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 @@ -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, @@ -209,6 +210,9 @@ public data class StreamDimens( participantLabelTextPaddingStart = dimensionResource( id = R.dimen.stream_video_callParticipantSoundIndicatorPaddingStart, ), + participantsGridPadding = dimensionResource( + id = R.dimen.stream_video_participantsGridPadding, + ), 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-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() } } } 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..ddde26682f 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; @@ -203,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 { @@ -233,6 +240,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 } @@ -259,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 { @@ -271,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 { @@ -538,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 { @@ -557,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 { @@ -572,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 { @@ -583,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 { @@ -607,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 { @@ -902,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 { @@ -909,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 { @@ -924,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 { @@ -943,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 { @@ -962,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 { @@ -1037,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; } @@ -1047,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; } @@ -1763,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 { @@ -2094,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/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/CallState.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt index d26e9a7bdc..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 @@ -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 @@ -119,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 @@ -135,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 @@ -143,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 } /** @@ -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/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 a744bff5c3..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() } /** @@ -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( 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-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 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() } } 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() } } } }