diff --git a/application-client/openvidu-android/.gitignore b/application-client/openvidu-android/.gitignore new file mode 100644 index 00000000..aa724b77 --- /dev/null +++ b/application-client/openvidu-android/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/application-client/openvidu-android/.idea/.gitignore b/application-client/openvidu-android/.idea/.gitignore new file mode 100644 index 00000000..26d33521 --- /dev/null +++ b/application-client/openvidu-android/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/application-client/openvidu-android/.idea/.name b/application-client/openvidu-android/.idea/.name new file mode 100644 index 00000000..a4b2ad72 --- /dev/null +++ b/application-client/openvidu-android/.idea/.name @@ -0,0 +1 @@ +Basic Android \ No newline at end of file diff --git a/application-client/openvidu-android/.idea/compiler.xml b/application-client/openvidu-android/.idea/compiler.xml new file mode 100644 index 00000000..b589d56e --- /dev/null +++ b/application-client/openvidu-android/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/application-client/openvidu-android/.idea/deploymentTargetSelector.xml b/application-client/openvidu-android/.idea/deploymentTargetSelector.xml new file mode 100644 index 00000000..bae10838 --- /dev/null +++ b/application-client/openvidu-android/.idea/deploymentTargetSelector.xml @@ -0,0 +1,18 @@ + + + + + + + + + \ No newline at end of file diff --git a/application-client/openvidu-android/.idea/gradle.xml b/application-client/openvidu-android/.idea/gradle.xml new file mode 100644 index 00000000..0897082f --- /dev/null +++ b/application-client/openvidu-android/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/application-client/openvidu-android/.idea/kotlinc.xml b/application-client/openvidu-android/.idea/kotlinc.xml new file mode 100644 index 00000000..fdf8d994 --- /dev/null +++ b/application-client/openvidu-android/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/application-client/openvidu-android/.idea/migrations.xml b/application-client/openvidu-android/.idea/migrations.xml new file mode 100644 index 00000000..f8051a6f --- /dev/null +++ b/application-client/openvidu-android/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/application-client/openvidu-android/.idea/misc.xml b/application-client/openvidu-android/.idea/misc.xml new file mode 100644 index 00000000..0ad17cbd --- /dev/null +++ b/application-client/openvidu-android/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/application-client/openvidu-android/.idea/other.xml b/application-client/openvidu-android/.idea/other.xml new file mode 100644 index 00000000..0d3a1fbb --- /dev/null +++ b/application-client/openvidu-android/.idea/other.xml @@ -0,0 +1,263 @@ + + + + + + \ No newline at end of file diff --git a/application-client/openvidu-android/.idea/vcs.xml b/application-client/openvidu-android/.idea/vcs.xml new file mode 100644 index 00000000..b2bdec2d --- /dev/null +++ b/application-client/openvidu-android/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/application-client/openvidu-android/app/.gitignore b/application-client/openvidu-android/app/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/application-client/openvidu-android/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/application-client/openvidu-android/app/build.gradle.kts b/application-client/openvidu-android/app/build.gradle.kts new file mode 100644 index 00000000..237e65f5 --- /dev/null +++ b/application-client/openvidu-android/app/build.gradle.kts @@ -0,0 +1,59 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.jetbrains.kotlin.android) + alias(libs.plugins.jetbrains.kotlin.plugin.serialization) +} + +android { + namespace = "io.openvidu.android" + compileSdk = 34 + + defaultConfig { + applicationId = "io.openvidu.android" + 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_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + viewBinding = true + } +} + +dependencies { + implementation(libs.livekit.android) + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.cio) + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.serialization.kotlinx.json) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.material) + implementation(libs.androidx.activity) + implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.annotation) + implementation(libs.androidx.lifecycle.livedata.ktx) + implementation(libs.androidx.lifecycle.viewmodel.ktx) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) +} \ No newline at end of file diff --git a/application-client/openvidu-android/app/proguard-rules.pro b/application-client/openvidu-android/app/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/application-client/openvidu-android/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/application-client/openvidu-android/app/src/androidTest/java/io/openvidu/android/ExampleInstrumentedTest.kt b/application-client/openvidu-android/app/src/androidTest/java/io/openvidu/android/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..a91e9916 --- /dev/null +++ b/application-client/openvidu-android/app/src/androidTest/java/io/openvidu/android/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package io.openvidu.android + +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("io.openvidu.android", appContext.packageName) + } +} \ No newline at end of file diff --git a/application-client/openvidu-android/app/src/main/AndroidManifest.xml b/application-client/openvidu-android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..5eab0157 --- /dev/null +++ b/application-client/openvidu-android/app/src/main/AndroidManifest.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application-client/openvidu-android/app/src/main/ic_launcher-playstore.png b/application-client/openvidu-android/app/src/main/ic_launcher-playstore.png new file mode 100644 index 00000000..68e0e28c Binary files /dev/null and b/application-client/openvidu-android/app/src/main/ic_launcher-playstore.png differ diff --git a/application-client/openvidu-android/app/src/main/java/io/openvidu/android/MainActivity.kt b/application-client/openvidu-android/app/src/main/java/io/openvidu/android/MainActivity.kt new file mode 100644 index 00000000..51fa1a97 --- /dev/null +++ b/application-client/openvidu-android/app/src/main/java/io/openvidu/android/MainActivity.kt @@ -0,0 +1,81 @@ +package io.openvidu.android + +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.widget.Button +import android.widget.EditText +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.floatingactionbutton.FloatingActionButton + +class MainActivity : AppCompatActivity() { + private lateinit var participantField: EditText + private lateinit var roomField: EditText + private lateinit var joinButton: Button + + private var applicationServerUrl = "https://192-168-1-136.openvidu-local.dev:6443/" + private var livekitUrl = "wss://192-168-1-136.openvidu-local.dev:7443/" + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + participantField = findViewById(R.id.participantName) + roomField = findViewById(R.id.roomName) + joinButton = findViewById(R.id.joinButton) + val settingsButton = findViewById(R.id.settingsButton) + + participantField.setText("Participant %d".format((1..100).random())) + + joinButton.setOnClickListener { + navigateToRoomLayoutActivity() + } + + settingsButton.setOnClickListener { + showSettingsDialog() + } + } + + private fun navigateToRoomLayoutActivity() { + joinButton.isEnabled = false + + val participantName = participantField.text.toString() + val roomName = roomField.text.toString() + + if (participantName.isNotEmpty() && roomName.isNotEmpty()) { + val intent = Intent(this, RoomLayoutActivity::class.java) + intent.putExtra("participantName", participantName) + intent.putExtra("roomName", roomName) + intent.putExtra("serverUrl", applicationServerUrl) + intent.putExtra("livekitUrl", livekitUrl) + startActivity(intent) + } + + joinButton.isEnabled = true + } + + private fun showSettingsDialog() { + val dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_settings, null) + val serverUrl = dialogView.findViewById(R.id.serverUrl) + val liveKitUrl = dialogView.findViewById(R.id.livekitUrl) + + serverUrl.setText(applicationServerUrl) + liveKitUrl.setText(livekitUrl) + + val builder = AlertDialog.Builder(this) + builder.setTitle("Configure URLs") + .setView(dialogView) + .setPositiveButton("Save") { dialog, _ -> + applicationServerUrl = serverUrl.text.toString() + livekitUrl = liveKitUrl.text.toString() + dialog.dismiss() + } + .setNegativeButton("Cancel") { dialog, _ -> + dialog.dismiss() + } + + val dialog = builder.create() + dialog.show() + } +} \ No newline at end of file diff --git a/application-client/openvidu-android/app/src/main/java/io/openvidu/android/RoomLayoutActivity.kt b/application-client/openvidu-android/app/src/main/java/io/openvidu/android/RoomLayoutActivity.kt new file mode 100644 index 00000000..ab08c0f2 --- /dev/null +++ b/application-client/openvidu-android/app/src/main/java/io/openvidu/android/RoomLayoutActivity.kt @@ -0,0 +1,181 @@ +package io.openvidu.android + +import android.Manifest +import android.content.pm.PackageManager +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.ContentType +import io.ktor.http.contentType +import io.ktor.serialization.kotlinx.json.json +import io.livekit.android.LiveKit +import io.livekit.android.events.RoomEvent +import io.livekit.android.events.collect +import io.livekit.android.room.Room +import io.livekit.android.renderer.SurfaceViewRenderer +import io.livekit.android.room.track.VideoTrack +import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable + +class RoomLayoutActivity : AppCompatActivity() { + private lateinit var APPLICATION_SERVER_URL: String + private lateinit var LIVEKIT_URL: String + + private lateinit var room: Room + + private val client = HttpClient(CIO) { + expectSuccess = true + install(ContentNegotiation) { + json() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_room_layout) + + APPLICATION_SERVER_URL = intent.getStringExtra("serverUrl") ?: "" + LIVEKIT_URL = intent.getStringExtra("livekitUrl") ?: "" + + // Create Room object. + room = LiveKit.create(applicationContext) + + // Setup the video renderer + room.initVideoRenderer(findViewById(R.id.renderer)) + + requestNeededPermissions { connectToRoom() } + } + + private fun connectToRoom() { + val participantName = intent.getStringExtra("participantName") ?: "Participant 1" + val roomName = intent.getStringExtra("roomName") ?: "Test Room" + + lifecycleScope.launch { + // Setup event handling. + launch { + room.events.collect { event -> + when (event) { + is RoomEvent.TrackSubscribed -> onTrackSubscribed(event) + is RoomEvent.TrackUnsubscribed -> onTrackUnsubscribed(event) + else -> {} + } + } + } + + try { + // Get token from server. + val token = getToken(roomName, participantName) + + // Connect to server. + room.connect(LIVEKIT_URL, token) + + // Turn on audio/video recording. + val localParticipant = room.localParticipant + localParticipant.setMicrophoneEnabled(true) + localParticipant.setCameraEnabled(true) + } catch (e: Exception) { + println("There was an error connecting to the room: ${e.message}") + Toast.makeText(this@RoomLayoutActivity, "Failed to join room", Toast.LENGTH_SHORT) + .show() + leaveRoom() + } + } + } + + private fun onTrackSubscribed(event: RoomEvent.TrackSubscribed) { + val track = event.track + + if (track is VideoTrack) { + attachVideo(track) + } + } + + private fun attachVideo(videoTrack: VideoTrack) { + videoTrack.addRenderer(findViewById(R.id.renderer)) + findViewById(R.id.progress).visibility = View.GONE + } + + private fun onTrackUnsubscribed(event: RoomEvent.TrackUnsubscribed) { + val track = event.track + + if (track is VideoTrack) { + detachVideo(track) + } + } + + private fun detachVideo(videoTrack: VideoTrack) { + videoTrack.removeRenderer(findViewById(R.id.renderer)) + findViewById(R.id.progress).visibility = View.VISIBLE + } + + private fun leaveRoom() { + room.disconnect() + client.close() + // Go back to the previous activity. + finish() + } + + override fun onDestroy() { + super.onDestroy() + leaveRoom() + } + + private fun requestNeededPermissions(onHasPermissions: () -> Unit) { + val requestPermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { grants -> + var hasDenied = false + + // Check if any permissions weren't granted. + for (grant in grants.entries) { + if (!grant.value) { + Toast.makeText(this, "Missing permission: ${grant.key}", Toast.LENGTH_SHORT) + .show() + + hasDenied = true + } + } + + if (!hasDenied) { + onHasPermissions() + } + } + + // Assemble the needed permissions to request + val neededPermissions = + listOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA).filter { + ContextCompat.checkSelfPermission( + this, it + ) == PackageManager.PERMISSION_DENIED + }.toTypedArray() + + if (neededPermissions.isNotEmpty()) { + requestPermissionLauncher.launch(neededPermissions) + } else { + onHasPermissions() + } + } + + private suspend fun getToken(roomName: String, participantName: String): String { + val response = client.post(APPLICATION_SERVER_URL + "token") { + contentType(ContentType.Application.Json) + setBody(TokenRequest(participantName, roomName)) + } + return response.body().token + } +} + +@Serializable +data class TokenRequest(val participantName: String, val roomName: String) + +@Serializable +data class TokenResponse(val token: String) diff --git a/application-client/openvidu-android/app/src/main/res/drawable/ic_launcher_background.xml b/application-client/openvidu-android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..07d5da9c --- /dev/null +++ b/application-client/openvidu-android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/application-client/openvidu-android/app/src/main/res/drawable/ic_launcher_foreground.xml b/application-client/openvidu-android/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000..2b068d11 --- /dev/null +++ b/application-client/openvidu-android/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/application-client/openvidu-android/app/src/main/res/drawable/ic_settings.xml b/application-client/openvidu-android/app/src/main/res/drawable/ic_settings.xml new file mode 100644 index 00000000..6abc6b78 --- /dev/null +++ b/application-client/openvidu-android/app/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/application-client/openvidu-android/app/src/main/res/layout/activity_main.xml b/application-client/openvidu-android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..10880abb --- /dev/null +++ b/application-client/openvidu-android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + +