diff --git a/interactive-live-streaming/android-ils/.gitignore b/interactive-live-streaming/android-ils/.gitignore new file mode 100644 index 0000000..7678611 --- /dev/null +++ b/interactive-live-streaming/android-ils/.gitignore @@ -0,0 +1,17 @@ +*.iml +.gradle +/local.properties +/.idea +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties +**.DS_STORE \ No newline at end of file diff --git a/interactive-live-streaming/android-ils/app/.gitignore b/interactive-live-streaming/android-ils/app/.gitignore new file mode 100644 index 0000000..7678611 --- /dev/null +++ b/interactive-live-streaming/android-ils/app/.gitignore @@ -0,0 +1,17 @@ +*.iml +.gradle +/local.properties +/.idea +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties +**.DS_STORE \ No newline at end of file diff --git a/interactive-live-streaming/android-ils/app/build.gradle.kts b/interactive-live-streaming/android-ils/app/build.gradle.kts new file mode 100644 index 0000000..476bd74 --- /dev/null +++ b/interactive-live-streaming/android-ils/app/build.gradle.kts @@ -0,0 +1,63 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "com.example.videosdk_android_modes_quickstart" + compileSdk = 34 + + defaultConfig { + applicationId = "com.example.videosdk_android_modes_quickstart" + minSdk = 24 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + compose = true + } +} + +dependencies { + implementation ("live.videosdk:rtc-android-sdk:0.2.0") + implementation ("com.nabinbhandari.android:permissions:3.8") + implementation ("com.amitshekhar.android:android-networking:1.0.2") + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + implementation(libs.androidx.navigation.runtime.ktx) + implementation(libs.androidx.navigation.compose) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) +} \ No newline at end of file diff --git a/interactive-live-streaming/android-ils/app/proguard-rules.pro b/interactive-live-streaming/android-ils/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/interactive-live-streaming/android-ils/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/interactive-live-streaming/android-ils/app/src/androidTest/java/com/example/videosdk_android_modes_quickstart/ExampleInstrumentedTest.kt b/interactive-live-streaming/android-ils/app/src/androidTest/java/com/example/videosdk_android_modes_quickstart/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..e7d2649 --- /dev/null +++ b/interactive-live-streaming/android-ils/app/src/androidTest/java/com/example/videosdk_android_modes_quickstart/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.example.videosdk_android_modes_quickstart + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.example.videosdk_android_modes_quickstart", appContext.packageName) + } +} \ No newline at end of file diff --git a/interactive-live-streaming/android-ils/app/src/main/AndroidManifest.xml b/interactive-live-streaming/android-ils/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..ca75a97 --- /dev/null +++ b/interactive-live-streaming/android-ils/app/src/main/AndroidManifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/MainActivity.kt b/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/MainActivity.kt new file mode 100644 index 0000000..2edc67b --- /dev/null +++ b/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/MainActivity.kt @@ -0,0 +1,65 @@ +package com.example.videosdk_android_modes_quickstart + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.runtime.Composable +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.example.videosdk_android_modes_quickstart.model.StreamViewModel +import com.example.videosdk_android_modes_quickstart.screens.JoinScreen +import com.example.videosdk_android_modes_quickstart.screens.StreamingScreen +import com.example.videosdk_android_modes_quickstart.ui.theme.Videosdk_android_modes_quickstartTheme + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + checkSelfPermission(REQUESTED_PERMISSIONS[0], PERMISSION_REQ_ID) + checkSelfPermission(REQUESTED_PERMISSIONS[1], PERMISSION_REQ_ID) + + setContent { + Videosdk_android_modes_quickstartTheme { + MyApp(this) } } + } + + private fun checkSelfPermission(permission: String, requestCode: Int): Boolean { + if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, REQUESTED_PERMISSIONS, requestCode) + return false + } + return true + } + + companion object { + private const val PERMISSION_REQ_ID = 22 + private val REQUESTED_PERMISSIONS = arrayOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA) + } +} + +@Composable +fun MyApp(context: Context) { + NavigationGraph(context = context) +} + +@Composable +fun NavigationGraph(navController: NavHostController = rememberNavController(), context: Context) { + NavHost(navController = navController, startDestination = "join_screen") { + composable("join_screen") { JoinScreen(navController, context) } + composable("stream_screen?streamId={streamId}&mode={mode}") { backStackEntry -> + val streamId = backStackEntry.arguments?.getString("streamId") + val modeStr = backStackEntry.arguments?.getString("mode") + val mode = StreamingMode.valueOf(modeStr ?: StreamingMode.SendAndReceive.name) + + streamId?.let { + StreamingScreen(viewModel = StreamViewModel(), navController, streamId, mode, context) + } + } + } +} \ No newline at end of file diff --git a/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/MainApplication.kt b/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/MainApplication.kt new file mode 100644 index 0000000..aa1d753 --- /dev/null +++ b/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/MainApplication.kt @@ -0,0 +1,15 @@ +package com.example.videosdk_android_modes_quickstart + +import android.app.Application +import live.videosdk.rtc.android.VideoSDK + +class MainApplication: Application() { + + + val sampleToken = "SAMPLE TOKEN" //Sample Token From VideoSDK + + override fun onCreate() { + super.onCreate() + VideoSDK.initialize(applicationContext) + } +} diff --git a/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/StreamingMode.kt b/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/StreamingMode.kt new file mode 100644 index 0000000..dce1207 --- /dev/null +++ b/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/StreamingMode.kt @@ -0,0 +1,6 @@ +package com.example.videosdk_android_modes_quickstart + +enum class StreamingMode { + SendAndReceive, + ReceiveOnly +} diff --git a/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/componenets/ParticipantVideoView.kt b/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/componenets/ParticipantVideoView.kt new file mode 100644 index 0000000..60f9071 --- /dev/null +++ b/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/componenets/ParticipantVideoView.kt @@ -0,0 +1,143 @@ +package com.example.videosdk_android_modes_quickstart.componenets + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +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.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect + +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.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import live.videosdk.rtc.android.Participant +import live.videosdk.rtc.android.Stream +import live.videosdk.rtc.android.VideoView +import live.videosdk.rtc.android.listeners.ParticipantEventListener +import org.webrtc.VideoTrack + +@Composable +fun ParticipantVideoView( + participant: Participant +) { + var isVideoEnabled by remember { mutableStateOf(false) } + // Remember the event listener to prevent recreation on each recomposition + val eventListener = remember(participant) { + object : ParticipantEventListener() { + override fun onStreamEnabled(stream: Stream) { + if (stream.kind.equals("video", ignoreCase = true)) { + val videoTrack = stream.track as VideoTrack + isVideoEnabled = true + } + } + override fun onStreamDisabled(stream: Stream) { + if (stream.kind.equals("video", ignoreCase = true)) { + isVideoEnabled = false + } + } + } + } + + // Add and remove the event listener using side effects + DisposableEffect(participant, eventListener) { + participant.addEventListener(eventListener) + onDispose { participant.removeEventListener(eventListener) } + } + + // Initial video state check + LaunchedEffect(participant) { + val hasVideoStream = participant.streams.any { (_, stream) -> + stream.kind.equals("video", ignoreCase = true) && stream.track != null + } + isVideoEnabled = hasVideoStream + } + + Box( + modifier = Modifier.fillMaxWidth() + .height(200.dp) + .background(if (isVideoEnabled) Color.DarkGray else Color.Gray) + ) { + AndroidView( + factory = { context -> + VideoView(context).apply { + for ((_, stream) in participant.streams) { + if (stream.kind.equals("video", ignoreCase = true)) { + val videoTrack = stream.track as VideoTrack + addTrack(videoTrack) + isVideoEnabled = true + } + } + } + }, + update = { videoView -> + // Handle video track updates only + // The event listener is now managed separately + for ((_, stream) in participant.streams) { + if (stream.kind.equals( + "video", + ignoreCase = true + ) && stream.track != null + ) { + val videoTrack = stream.track as VideoTrack + videoView.addTrack(videoTrack) + } + } + }, + modifier = Modifier.fillMaxSize() + ) + + if (!isVideoEnabled) { + Box( + modifier = Modifier.fillMaxSize() + .background(Color.DarkGray), + contentAlignment = Alignment.Center + ) { + Text(text = "Camera Off", color = Color.White) + } + } + + Box( + modifier = Modifier.align(Alignment.BottomCenter) + .fillMaxWidth() + .background(Color(0x99000000)) + .padding(4.dp) + ) { + Text( + text = participant.displayName, + color = Color.White, + modifier = Modifier.align(Alignment.Center) + ) + } + } + +} + +@Composable +fun ParticipantsGrid( + participants: List, + modifier: Modifier = Modifier +) { + LazyVerticalGrid(columns = GridCells.Fixed(2), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier.fillMaxWidth() + .padding(8.dp) + ) { + items(participants.size) { index -> + ParticipantVideoView(participant = participants[index]) + } + } +} \ No newline at end of file diff --git a/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/componenets/ReusableComponents.kt b/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/componenets/ReusableComponents.kt new file mode 100644 index 0000000..e726e26 --- /dev/null +++ b/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/componenets/ReusableComponents.kt @@ -0,0 +1,40 @@ +package com.example.videosdk_android_modes_quickstart.componenets + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + + +@Composable +fun MyAppButton(task: () -> Unit, buttonName: String) { + Button(onClick = task) { + Text(text = buttonName) } +} + +@Composable +fun MySpacer() { + Spacer(modifier = Modifier.fillMaxWidth() + .height(1.dp) + .background(color = Color.Gray)) } + +@Composable +fun MyText(text: String, fontSize: TextUnit = 23.sp) { + Text(text = text, + fontSize = fontSize, + fontWeight = FontWeight.Normal, + modifier = Modifier.padding(4.dp), + style = MaterialTheme.typography.bodyMedium.copy(fontSize = 16.sp), + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)) +} \ No newline at end of file diff --git a/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/model/NetworkManager.kt b/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/model/NetworkManager.kt new file mode 100644 index 0000000..dcfac91 --- /dev/null +++ b/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/model/NetworkManager.kt @@ -0,0 +1,30 @@ +package com.example.videosdk_android_modes_quickstart.model + +import android.util.Log +import com.androidnetworking.AndroidNetworking +import com.androidnetworking.error.ANError +import com.androidnetworking.interfaces.JSONObjectRequestListener +import org.json.JSONException +import org.json.JSONObject + +object NetworkManager { + fun createStreamId(token: String, onStreamIdCreated: (String) -> Unit) { + AndroidNetworking.post("https://dev-api.videosdk.live/v2/rooms") + .addHeaders("Authorization", token) + .build() + .getAsJSONObject(object : JSONObjectRequestListener { + override fun onResponse(response: JSONObject) { + try { + val streamId = response.getString("roomId") + onStreamIdCreated(streamId) + } catch (e: JSONException) { + e.printStackTrace() + } + } + override fun onError(anError: ANError) { + anError.printStackTrace() + Log.d("TAG", "onError: $anError") + } + }) + } +} \ No newline at end of file diff --git a/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/model/StreamViewModel.kt b/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/model/StreamViewModel.kt new file mode 100644 index 0000000..ffe4920 --- /dev/null +++ b/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/model/StreamViewModel.kt @@ -0,0 +1,169 @@ +package com.example.videosdk_android_modes_quickstart.model + +import android.content.Context +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import com.example.videosdk_android_modes_quickstart.StreamingMode +import live.videosdk.rtc.android.Meeting +import live.videosdk.rtc.android.Participant +import live.videosdk.rtc.android.VideoSDK +import live.videosdk.rtc.android.listeners.MeetingEventListener +import org.json.JSONException +import org.json.JSONObject + +class StreamViewModel : ViewModel() { + private var stream: Meeting? = null + private var micEnabled by mutableStateOf(true) + private var webcamEnabled by mutableStateOf(true) + var isConferenceMode by mutableStateOf(true) + val currentParticipants = mutableStateListOf() + + var localParticipantId by mutableStateOf("") + private set + + var isStreamLeft by mutableStateOf(false) + private set + + var currentMode by mutableStateOf(StreamingMode.SendAndReceive) + private set + + var isJoined by mutableStateOf(false) + private set + + fun initStream(context: Context, token: String, streamId: String, mode: StreamingMode) { + VideoSDK.config(token) + currentMode = mode + if (stream == null) { + stream = if (mode.name == StreamingMode.SendAndReceive.name) { + VideoSDK.initMeeting( + context,streamId, "John Doe", + micEnabled, webcamEnabled, null, null, true, null, null + ) + } else { + VideoSDK.initMeeting( + context, streamId, "John Doe", + micEnabled, webcamEnabled, null, "RECV_ONLY", true, null, null + ) + } + } + stream!!.addEventListener(meetingEventListener) + stream!!.join() + isJoined = true + } + + private val meetingEventListener: MeetingEventListener = object : MeetingEventListener() { + override fun onMeetingJoined() { + stream?.let { + if (it.localParticipant.mode != "RECV_ONLY") { + if (!currentParticipants.contains(it.localParticipant)) { + currentParticipants.add(it.localParticipant) + } + } + localParticipantId = it.localParticipant.id + } + } + + override fun onMeetingLeft() { + currentParticipants.clear() + stream = null + isStreamLeft = true + } + + override fun onParticipantJoined(participant: Participant) { + if (participant.mode != "RECV_ONLY") { + currentParticipants.add(participant) + } + } + + override fun onParticipantLeft(participant: Participant) { + currentParticipants.remove(participant) + } + + override fun onParticipantModeChanged(data: JSONObject?) { + try { + val participantId = data?.getString("peerId") + val participant = if (stream?.localParticipant?.id == participantId) { + stream?.localParticipant + } else { + stream?.participants?.get(participantId) + } + + participant?.let { + when (it.mode) { + "RECV_ONLY" -> { + currentParticipants.remove(it) + } + "SEND_AND_RECV" -> { + if (!currentParticipants.contains(it)) { + currentParticipants.add(it) + } else { } + } + else -> {} + } + } + } catch (e: JSONException) { + e.printStackTrace() + } + } + } + + fun toggleMic() { + if (micEnabled) stream?.muteMic() else stream?.unmuteMic() + micEnabled = !micEnabled + } + + fun toggleWebcam() { + if (webcamEnabled) stream?.disableWebcam() else stream?.enableWebcam() + webcamEnabled = !webcamEnabled + } + + fun leaveStream() { + currentParticipants.clear() + stream?.leave() + stream?.removeAllListeners() + isStreamLeft = true + isJoined = false + } + + fun toggleMode() { + val newMode = if (currentMode == StreamingMode.SendAndReceive) { + stream?.disableWebcam() + stream?.changeMode("RECV_ONLY") + isConferenceMode = false + StreamingMode.ReceiveOnly + } else { + stream?.changeMode("SEND_AND_RECV") + isConferenceMode = true + StreamingMode.SendAndReceive + } + + // Clear all participants first + currentParticipants.clear() + + // Wait briefly for mode change to complete + stream?.let { it -> + // Add back all non-viewer participants + it.participants.values.forEach { participant -> + if (participant.mode != "RECV_ONLY") { + currentParticipants.add(participant) + } + } + + // Add local participant only if in SEND_AND_RECV mode + if (newMode == StreamingMode.SendAndReceive) { + it.localParticipant?.let { localParticipant -> + if (!currentParticipants.contains(localParticipant)) { + currentParticipants.add(localParticipant) + } + } + // Re-enable webcam if it was enabled before + if (webcamEnabled) { + it.enableWebcam() } + } + } + currentMode = newMode + } +} diff --git a/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/screens/JoinScreen.kt b/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/screens/JoinScreen.kt new file mode 100644 index 0000000..2a2c4b8 --- /dev/null +++ b/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/screens/JoinScreen.kt @@ -0,0 +1,86 @@ +package com.example.videosdk_android_modes_quickstart.screens + + +import android.content.Context +import androidx.compose.foundation.layout.Arrangement +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.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import com.example.videosdk_android_modes_quickstart.MainApplication +import com.example.videosdk_android_modes_quickstart.StreamingMode +import com.example.videosdk_android_modes_quickstart.componenets.MyAppButton +import com.example.videosdk_android_modes_quickstart.model.NetworkManager + + +@Composable +fun JoinScreen( + navController: NavController, context: Context +) { + val app = context.applicationContext as MainApplication + val token = app.sampleToken + Box( + modifier = Modifier.fillMaxSize() + .padding(8.dp), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier.padding(4.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceEvenly + ) { + var input by rememberSaveable { mutableStateOf("") } + CreateStreamBtn(navController, token) + Text(text = "OR") + InputStreamId(input) { updateInput -> input = updateInput } + JoinHostBtn(navController, input) + JoinAudienceBtn(navController, input) + } + } +} + + +@Composable +fun JoinHostBtn(navController: NavController, streamId: String) { + MyAppButton({ + if (streamId.isNotEmpty()) { + navController.navigate("stream_screen?streamId=$streamId&mode=${StreamingMode.SendAndReceive.name}") + } + }, "Join as Host") +} + +@Composable +fun JoinAudienceBtn(navController: NavController, streamId: String) { + MyAppButton({ + if (streamId.isNotEmpty()) { + navController.navigate("stream_screen?streamId=$streamId&mode=${StreamingMode.ReceiveOnly.name}") + } + }, "Join as Audience") +} + +@Composable +fun CreateStreamBtn(navController: NavController, token: String) { + MyAppButton({ + NetworkManager.createStreamId(token) { streamId -> + navController.navigate("stream_screen?streamId=$streamId&mode=${StreamingMode.SendAndReceive.name}") + } + }, "Create Stream") +} + +@Composable +fun InputStreamId(input: String, onInputChange: (String) -> Unit) { + OutlinedTextField(value = input, + onValueChange = onInputChange, + label = { Text(text = "Enter Stream Id") }) +} diff --git a/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/screens/StreamingScreen.kt b/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/screens/StreamingScreen.kt new file mode 100644 index 0000000..6eafbfc --- /dev/null +++ b/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/screens/StreamingScreen.kt @@ -0,0 +1,155 @@ +package com.example.videosdk_android_modes_quickstart.screens + + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import com.example.videosdk_android_modes_quickstart.MainApplication +import com.example.videosdk_android_modes_quickstart.R +import com.example.videosdk_android_modes_quickstart.StreamingMode +import com.example.videosdk_android_modes_quickstart.model.StreamViewModel +import com.example.videosdk_android_modes_quickstart.componenets.MyAppButton +import com.example.videosdk_android_modes_quickstart.componenets.MySpacer +import com.example.videosdk_android_modes_quickstart.componenets.MyText +import com.example.videosdk_android_modes_quickstart.componenets.ParticipantsGrid + + +@Composable +fun StreamingScreen(viewModel: StreamViewModel, navController: NavController, + streamId: String, + mode: StreamingMode, + context: Context +) { + val app = context.applicationContext as MainApplication + val isStreamLeft = viewModel.isStreamLeft + val currentMode = viewModel.currentMode + val isJoined = viewModel.isJoined + + LaunchedEffect(isStreamLeft) { + if (isStreamLeft) { + navController.navigate("join_screen") } + } + + Column(modifier = Modifier.fillMaxSize()) { + Header(streamId, currentMode) + MySpacer() + ParticipantsGrid( + participants = viewModel.currentParticipants, + modifier = Modifier.weight(1f) + ) + MySpacer() + MediaControlButtons( + onJoinClick = { viewModel.initStream(context, app.sampleToken, streamId, mode)}, + onMicClick = { viewModel.toggleMic() }, + onCamClick = { viewModel.toggleWebcam()}, + onModeToggleClick = { viewModel.toggleMode() }, + showMediaControls = currentMode == StreamingMode.SendAndReceive, + currentMode = currentMode, + onLeaveClick = { viewModel.leaveStream() }, + isJoined = isJoined ) + } +} +@Composable +fun Header(streamId: String, mode: StreamingMode) { + val context = LocalContext.current + val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + + Box(modifier = Modifier.fillMaxWidth() + .padding(8.dp), + contentAlignment = Alignment.TopStart + ) { + Row( modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column { + Row(verticalAlignment = Alignment.CenterVertically) { + MyText("StreamID: ", 28.sp) + MyText(streamId, 25.sp) + IconButton( onClick = { + val clip = ClipData.newPlainText("Stream ID", streamId) + clipboardManager.setPrimaryClip(clip) + }) + { + //get baseline_content_copy_24 icon from Vector Asset + Icon( painter = painterResource(R.drawable.baseline_content_copy_24), + contentDescription = "Copy Stream ID", + modifier = Modifier.size(40.dp).padding(start = 8.dp) + ) + } + } + Row { + MyText("Mode: ", 20.sp) + MyText(mode.name, 18.sp) + } + } + } + } +} + +@Composable +fun MediaControlButtons( + onJoinClick: () -> Unit, + onMicClick: () -> Unit, + onCamClick: () -> Unit, + onModeToggleClick: () -> Unit, + onLeaveClick: () -> Unit, + showMediaControls: Boolean = true, + currentMode: StreamingMode, + isJoined: Boolean +) { + Column( modifier = Modifier.fillMaxWidth() + .padding(6.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Row( modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + if (!isJoined) { + MyAppButton(onJoinClick, "Join") + } + if (showMediaControls && isJoined) { // Only show media controls if joined + MyAppButton(onMicClick, "ToggleMic") + MyAppButton(onCamClick, "ToggleCam") + } + } + + Row( modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + if (isJoined) { + MyAppButton(onLeaveClick, "Leave") + MyAppButton( + task = onModeToggleClick, + buttonName = if (currentMode == StreamingMode.SendAndReceive) + "Switch to Audience" + else + "Switch to Host" + ) + } + } + } +} \ No newline at end of file diff --git a/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/ui/theme/Color.kt b/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/ui/theme/Color.kt new file mode 100644 index 0000000..b8fdd21 --- /dev/null +++ b/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.example.videosdk_android_modes_quickstart.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/ui/theme/Theme.kt b/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/ui/theme/Theme.kt new file mode 100644 index 0000000..bc1c8f2 --- /dev/null +++ b/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/ui/theme/Theme.kt @@ -0,0 +1,58 @@ +package com.example.videosdk_android_modes_quickstart.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun Videosdk_android_modes_quickstartTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = false, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/ui/theme/Type.kt b/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/ui/theme/Type.kt new file mode 100644 index 0000000..4a61f58 --- /dev/null +++ b/interactive-live-streaming/android-ils/app/src/main/java/com/example/videosdk_android_modes_quickstart/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package com.example.videosdk_android_modes_quickstart.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/interactive-live-streaming/android-ils/app/src/main/res/drawable/baseline_content_copy_24.xml b/interactive-live-streaming/android-ils/app/src/main/res/drawable/baseline_content_copy_24.xml new file mode 100644 index 0000000..942aeb9 --- /dev/null +++ b/interactive-live-streaming/android-ils/app/src/main/res/drawable/baseline_content_copy_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/interactive-live-streaming/android-ils/app/src/main/res/drawable/ic_launcher_background.xml b/interactive-live-streaming/android-ils/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/interactive-live-streaming/android-ils/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/interactive-live-streaming/android-ils/app/src/main/res/drawable/ic_launcher_foreground.xml b/interactive-live-streaming/android-ils/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/interactive-live-streaming/android-ils/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/interactive-live-streaming/android-ils/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/interactive-live-streaming/android-ils/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/interactive-live-streaming/android-ils/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/interactive-live-streaming/android-ils/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/interactive-live-streaming/android-ils/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/interactive-live-streaming/android-ils/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/interactive-live-streaming/android-ils/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/interactive-live-streaming/android-ils/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/interactive-live-streaming/android-ils/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/interactive-live-streaming/android-ils/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/interactive-live-streaming/android-ils/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/interactive-live-streaming/android-ils/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/interactive-live-streaming/android-ils/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/interactive-live-streaming/android-ils/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/interactive-live-streaming/android-ils/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/interactive-live-streaming/android-ils/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/interactive-live-streaming/android-ils/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/interactive-live-streaming/android-ils/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/interactive-live-streaming/android-ils/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/interactive-live-streaming/android-ils/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/interactive-live-streaming/android-ils/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/interactive-live-streaming/android-ils/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/interactive-live-streaming/android-ils/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/interactive-live-streaming/android-ils/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/interactive-live-streaming/android-ils/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/interactive-live-streaming/android-ils/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/interactive-live-streaming/android-ils/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/interactive-live-streaming/android-ils/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/interactive-live-streaming/android-ils/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/interactive-live-streaming/android-ils/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/interactive-live-streaming/android-ils/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/interactive-live-streaming/android-ils/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/interactive-live-streaming/android-ils/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/interactive-live-streaming/android-ils/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/interactive-live-streaming/android-ils/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/interactive-live-streaming/android-ils/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/interactive-live-streaming/android-ils/app/src/main/res/values/colors.xml b/interactive-live-streaming/android-ils/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/interactive-live-streaming/android-ils/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/interactive-live-streaming/android-ils/app/src/main/res/values/strings.xml b/interactive-live-streaming/android-ils/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..e5c3a3b --- /dev/null +++ b/interactive-live-streaming/android-ils/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Videosdk_android_modes_quickstart + \ No newline at end of file diff --git a/interactive-live-streaming/android-ils/app/src/main/res/values/themes.xml b/interactive-live-streaming/android-ils/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..08d82a1 --- /dev/null +++ b/interactive-live-streaming/android-ils/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +