diff --git a/.gitignore b/.gitignore index f168259fbb..cb25b4a118 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,6 @@ .gradle /local.properties /.idea -/build +build/ .DS_Store /.kotlin \ No newline at end of file diff --git a/README.md b/README.md index 82d76d7bcb..1dce3979ed 100644 --- a/README.md +++ b/README.md @@ -180,3 +180,28 @@ https://github.com/pedroSG94/RootEncoder/tree/master/app/src/main/java/com/pedro Code example for low API devices (Android API 16+): https://github.com/pedroSG94/RootEncoder/tree/master/app/src/main/java/com/pedro/streamer/oldapi + +## DK Release and Deploy + +We use the [axion-release-plugin](https://axion-release-plugin.readthedocs.io/en/latest/) to help with versioning and releases. + +Basic workflow with axion-release: +``` +$ ./gradlew currentVersion +0.1.0 + +$ git commit -m "Some commit." + +$ ./gradlew currentVersion +0.1.1-SNAPSHOT + +// Create a new tag and push it to remote +$ ./gradlew release + +$ git tag +project-0.1.0 +project-0.1.1 + +// Upload release to dk-maven +$ ./gradlew publish +``` \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 754f333fa9..1fcb80abe9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -11,8 +11,6 @@ android { applicationId = "com.pedro.streamer" minSdk = 16 targetSdk = 36 - versionCode = project.version.toString().replace(".", "").toInt() - versionName = project.version.toString() multiDexEnabled = true } buildTypes { diff --git a/app/src/main/java/com/pedro/streamer/file/FromFileActivity.kt b/app/src/main/java/com/pedro/streamer/file/FromFileActivity.kt index c748397009..b956048dc8 100644 --- a/app/src/main/java/com/pedro/streamer/file/FromFileActivity.kt +++ b/app/src/main/java/com/pedro/streamer/file/FromFileActivity.kt @@ -31,6 +31,7 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import com.pedro.common.ConnectChecker +import com.pedro.common.Throughput import com.pedro.encoder.input.decoder.AudioDecoderInterface import com.pedro.encoder.input.decoder.VideoDecoderInterface import com.pedro.library.base.recording.RecordController @@ -194,6 +195,13 @@ class FromFileActivity : AppCompatActivity(), ConnectChecker, override fun onNewBitrate(bitrate: Long) {} + override fun onStreamingStats( + bitrate: Long, + bytesSent: Long, + bytesQueued: Long, + throughput: Throughput + ) { + } override fun onDisconnect() { toast("Disconnected") } diff --git a/app/src/main/java/com/pedro/streamer/oldapi/OldApiActivity.kt b/app/src/main/java/com/pedro/streamer/oldapi/OldApiActivity.kt index 78e159346c..c3f310b329 100644 --- a/app/src/main/java/com/pedro/streamer/oldapi/OldApiActivity.kt +++ b/app/src/main/java/com/pedro/streamer/oldapi/OldApiActivity.kt @@ -25,6 +25,7 @@ import android.widget.ImageView import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import com.pedro.common.ConnectChecker +import com.pedro.common.Throughput import com.pedro.encoder.input.video.CameraHelper import com.pedro.encoder.input.video.CameraOpenException import com.pedro.library.base.recording.RecordController @@ -151,6 +152,13 @@ class OldApiActivity : AppCompatActivity(), ConnectChecker, TextureView.SurfaceT override fun onNewBitrate(bitrate: Long) {} + override fun onStreamingStats( + bitrate: Long, + bytesSent: Long, + bytesQueued: Long, + throughput: Throughput + ) { + } override fun onDisconnect() { toast("Disconnected") } diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts new file mode 100644 index 0000000000..a6dd71876b --- /dev/null +++ b/build-logic/convention/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + `kotlin-dsl` +} + +repositories { + google() + mavenCentral() + gradlePluginPortal() +} + +dependencies { + compileOnly("com.android.tools.build:gradle:8.2.2") + implementation("software.amazon.awssdk:codeartifact:2.20.68") + implementation("pl.allegro.tech.build:axion-release-plugin:1.15.4") +} + +gradlePlugin { + plugins { + register("projectVersioning") { + id = "convention.project.versioning" + implementationClass = "plugins.VersioningConventionPlugin" + } + register("projectPublishing") { + id = "convention.publishing" + implementationClass = "plugins.PublishingConventionPlugin" + } + } +} diff --git a/build-logic/convention/src/main/kotlin/extensions/PublishingConfigExtension.kt b/build-logic/convention/src/main/kotlin/extensions/PublishingConfigExtension.kt new file mode 100644 index 0000000000..88a648ba23 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/extensions/PublishingConfigExtension.kt @@ -0,0 +1,30 @@ +package extensions + +open class PublishingConfigExtension( + var groupId: String = "", + var artifactId: String = "", + // CodeArtifact specific + var domain: String = "", + var domainOwner: String = "", + var repository: String = "", + var region: String = "us-east-1", +) { + fun groupId(value: String) { + groupId = value + } + fun artifactId(value: String) { + artifactId = value + } + fun domain(value: String) { + domain = value + } + fun domainOwner(value: String) { + domainOwner = value + } + fun repository(value: String) { + repository = value + } + fun region(value: String) { + region = value + } +} diff --git a/build-logic/convention/src/main/kotlin/extensions/VersioningExtension.kt b/build-logic/convention/src/main/kotlin/extensions/VersioningExtension.kt new file mode 100644 index 0000000000..8f09be625d --- /dev/null +++ b/build-logic/convention/src/main/kotlin/extensions/VersioningExtension.kt @@ -0,0 +1,20 @@ +package extensions + +open class VersioningExtension( + var tagPrefix: String = "", + var useHighestVersion: Boolean = true, + var initialVersion: String = "0.1.0", +) { + + fun tagPrefix(value: String) { + tagPrefix = value + } + + fun useHighestVersion(value: Boolean) { + useHighestVersion = value + } + + fun initialVersion(value: String) { + initialVersion = value + } +} diff --git a/build-logic/convention/src/main/kotlin/plugins/PublishingConventionPlugin.kt b/build-logic/convention/src/main/kotlin/plugins/PublishingConventionPlugin.kt new file mode 100644 index 0000000000..dd4c011d19 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/plugins/PublishingConventionPlugin.kt @@ -0,0 +1,75 @@ +package plugins + +import extensions.PublishingConfigExtension +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.publish.PublishingExtension +import org.gradle.api.publish.maven.MavenPublication +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.create +import org.gradle.kotlin.dsl.get +import software.amazon.awssdk.regions.Region +import software.amazon.awssdk.services.codeartifact.CodeartifactClient +import software.amazon.awssdk.services.codeartifact.model.GetAuthorizationTokenRequest + +@Suppress("unused") +class PublishingConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("maven-publish") + } + + val extension = extensions.create("publishingConfig", PublishingConfigExtension::class.java) + + afterEvaluate { + configurePublishing(extension) + } + } + } + + private fun Project.configurePublishing(extension: PublishingConfigExtension) { + extensions.configure { + publications { + // Creates a Maven publication called "release". + create("release") { + // Applies the component for the release build variant. + from(components["release"]) + + // You can then customize attributes of the publication as shown below. + groupId = extension.groupId + artifactId = extension.artifactId + } + } + repositories { + // CodeArtifact repository + maven { + name = "CodeArtifact" + url = project.uri(getCodeArtifactRepoUrl(extension)) + credentials { + username = "aws" + password = getCodeArtifactAuthToken(extension) + } + } + } + } + } + + @Suppress("ktlint:standard:max-line-length") + private fun getCodeArtifactRepoUrl(extension: PublishingConfigExtension): String { + return "https://${extension.domain}-${extension.domainOwner}.d.codeartifact.${extension.region}.amazonaws.com/maven/${extension.repository}/" + } + + private fun getCodeArtifactAuthToken(extension: PublishingConfigExtension): String { + val client = CodeartifactClient.builder() + .region(Region.of(extension.region)) + .build() + + val request = GetAuthorizationTokenRequest.builder() + .domain(extension.domain) + .domainOwner(extension.domainOwner) + .build() + + return client.getAuthorizationToken(request).authorizationToken() + } +} diff --git a/build-logic/convention/src/main/kotlin/plugins/VersioningConventionPlugin.kt b/build-logic/convention/src/main/kotlin/plugins/VersioningConventionPlugin.kt new file mode 100644 index 0000000000..c7d867189d --- /dev/null +++ b/build-logic/convention/src/main/kotlin/plugins/VersioningConventionPlugin.kt @@ -0,0 +1,75 @@ +package plugins + +import com.android.build.gradle.AppExtension +import com.android.build.gradle.BaseExtension +import com.android.build.gradle.LibraryExtension +import extensions.VersioningExtension +import org.gradle.api.GradleException +import org.gradle.api.Plugin +import org.gradle.api.Project +import pl.allegro.tech.build.axion.release.ReleasePlugin +import pl.allegro.tech.build.axion.release.domain.VersionConfig + +@Suppress("unused") +class VersioningConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + if (this != rootProject) { + throw GradleException( + "Versioning plugin can be applied to the root project only", + ) + } + + with(pluginManager) { + apply(ReleasePlugin::class.java) + } + + val extension = extensions.create("versioning", VersioningExtension::class.java) + + afterEvaluate { + configureVersioning(extension) + } + } + } + + private fun Project.configureVersioning(extension: VersioningExtension) { + val scmConfig = extensions.getByType(VersionConfig::class.java).apply { + useHighestVersion.set(extension.useHighestVersion) + versionCreator("versionWithBranch") + tag { + prefix.set(extension.tagPrefix) + versionSeparator.set("") + initialVersion.set({ _, _ -> extension.initialVersion }) + } + } + allprojects { + version = scmConfig.version + setupAndroidVersioning(scmConfig) + } + } + + private fun Project.setupAndroidVersioning(scmConfig: VersionConfig) { + val configureVersion: BaseExtension.(String) -> Unit = { version -> + val minor = version.split(".")[0].toInt() + val major = version.split(".")[1].toInt() + val patch = version.split(".")[2].toInt() + defaultConfig.versionCode = minor * MINOR_MULTIPLIER + major * MAJOR_MULTIPLIER + patch + defaultConfig.versionName = "$minor.$major.$patch" + } + pluginManager.withPlugin("com.android.library") { + extensions.getByType(LibraryExtension::class.java).apply { + configureVersion(scmConfig.undecoratedVersion) + } + } + pluginManager.withPlugin("com.android.application") { + extensions.getByType(AppExtension::class.java).apply { + configureVersion(scmConfig.undecoratedVersion) + } + } + } + + companion object { + private const val MINOR_MULTIPLIER = 1_000_000 + private const val MAJOR_MULTIPLIER = 1_000 + } +} diff --git a/build-logic/gradle.properties b/build-logic/gradle.properties new file mode 100644 index 0000000000..48b2ab833a --- /dev/null +++ b/build-logic/gradle.properties @@ -0,0 +1,4 @@ +# Gradle properties are not passed to included builds https://github.com/gradle/gradle/issues/2534 +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.configureondemand=true \ No newline at end of file diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts new file mode 100644 index 0000000000..7e16a5ea2e --- /dev/null +++ b/build-logic/settings.gradle.kts @@ -0,0 +1,19 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + @Suppress("UnstableApiUsage") + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = "build-logic" +include(":convention") diff --git a/build.gradle.kts b/build.gradle.kts index f63579a7e1..44b552fd20 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,6 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. allprojects { - group = "com.github.pedroSG94" + group = "com.github.pedroSG94.RootEncoder" version = "2.6.4" } @@ -8,6 +9,8 @@ plugins { alias(libs.plugins.android.library) apply false alias(libs.plugins.jetbrains.kotlin) apply false alias(libs.plugins.jetbrains.dokka) apply true + + id("convention.project.versioning") } tasks.register("clean") { diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 1846510320..70c2d036e5 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -3,6 +3,8 @@ plugins { alias(libs.plugins.jetbrains.kotlin) alias(libs.plugins.jetbrains.dokka) `maven-publish` + + id("convention.publishing") } android { @@ -31,21 +33,13 @@ android { } } -afterEvaluate { - publishing { - publications { - // Creates a Maven publication called "release". - create("release") { - // Applies the component for the release build variant. - from(components["release"]) - - // You can then customize attributes of the publication as shown below. - groupId = project.group.toString() - artifactId = project.name - version = project.version.toString() - } - } - } +publishingConfig { + groupId = group.toString() + artifactId = "common" + domain = "diamond-kinetics" + domainOwner = "626803233223" + repository = "dk-maven" + region = "us-east-1" } dependencies { diff --git a/common/src/main/java/com/pedro/common/BitrateChecker.java b/common/src/main/java/com/pedro/common/BitrateChecker.java index a330bc6fad..af0f57974f 100644 --- a/common/src/main/java/com/pedro/common/BitrateChecker.java +++ b/common/src/main/java/com/pedro/common/BitrateChecker.java @@ -21,4 +21,6 @@ */ public interface BitrateChecker { default void onNewBitrate(long bitrate) {} + + default void onStreamingStats(long bitrate, long bytesSent, long bytesQueued, Throughput throughput) {} } diff --git a/common/src/main/java/com/pedro/common/BitrateManager.kt b/common/src/main/java/com/pedro/common/BitrateManager.kt index 49a64a9c0f..e237ee1e66 100644 --- a/common/src/main/java/com/pedro/common/BitrateManager.kt +++ b/common/src/main/java/com/pedro/common/BitrateManager.kt @@ -30,6 +30,63 @@ open class BitrateManager(private val bitrateChecker: BitrateChecker) { var exponentialFactor: Float = 1f private var timeStamp = TimeUtils.getCurrentTimeMillis() + private var byterate = 0L + private var queuedBytes: Long = 0 + private val measureInterval = 3 + private val previousQueueBytesOut: MutableList = mutableListOf() + + fun queueBytes(size: Long) { + queuedBytes += size + } + + suspend fun calculateBitrateAndBandwidth(bytesSendPerSecond: Long, myQueueValue: Long) { + // Track bitrate and queue management + bitrate += (bytesSendPerSecond * 8) + byterate += bytesSendPerSecond + queuedBytes -= bytesSendPerSecond + + val timeDiff = TimeUtils.getCurrentTimeMillis() - timeStamp + if (timeDiff >= 1000) { + // Calculate bitrate with exponential moving average + val currentValue = (bitrate / (timeDiff / 1000f)).toLong() + if (bitrateOld == 0L) { bitrateOld = currentValue } + bitrateOld = (bitrateOld + exponentialFactor * (currentValue - bitrateOld)).toLong() + + // Calculate bandwidth and throughput analysis + var throughput: Throughput = Throughput.Unknown + previousQueueBytesOut.add(queuedBytes) + if (measureInterval <= previousQueueBytesOut.size) { + var countQueuedBytesGrowing = 0 + for (i in 0 until previousQueueBytesOut.size - 1) { + if (previousQueueBytesOut[i] < previousQueueBytesOut[i + 1]) { + countQueuedBytesGrowing++ + } + } + if (countQueuedBytesGrowing == measureInterval - 1) { + throughput = Throughput.Insufficient + } else if (countQueuedBytesGrowing == 0) { + throughput = Throughput.Sufficient + } + previousQueueBytesOut.removeAt(0) + } + + // Call both callbacks on main thread + onMainThread { + bitrateChecker.onNewBitrate(bitrateOld) + bitrateChecker.onStreamingStats( + ((bytesSendPerSecond * 8) / (timeDiff / 1000f)).toLong(), + bytesSendPerSecond, + myQueueValue, + throughput + ) + } + + // Reset all counters + timeStamp = TimeUtils.getCurrentTimeMillis() + bitrate = 0 + } + } + suspend fun calculateBitrate(size: Long) { bitrate += size val timeDiff = TimeUtils.getCurrentTimeMillis() - timeStamp @@ -44,7 +101,9 @@ open class BitrateManager(private val bitrateChecker: BitrateChecker) { } fun reset() { - bitrate = 0 + byterate = 0 bitrateOld = 0 + queuedBytes = 0 + previousQueueBytesOut.clear() } } \ No newline at end of file diff --git a/common/src/main/java/com/pedro/common/StreamBlockingQueue.kt b/common/src/main/java/com/pedro/common/StreamBlockingQueue.kt index 52499bb3a3..ff64308c91 100644 --- a/common/src/main/java/com/pedro/common/StreamBlockingQueue.kt +++ b/common/src/main/java/com/pedro/common/StreamBlockingQueue.kt @@ -59,4 +59,20 @@ class StreamBlockingQueue(size: Int) { } fun getSize() = queue.size + + + /** + * Gets the total size of all items in the queue by summing the individual sizes. + * + * This method calculates the cumulative size of all [MediaFrame] objects currently + * in the queue by summing their [MediaFrame.info.size] values. This is useful for + * monitoring memory usage, bandwidth calculations, or determining the total data + * volume in the queue. + * + * @return The total size in bytes as a [Long] value, representing the sum of all + * individual frame sizes in the queue. + * + * @see getSize for getting the count of items in the queue + */ + fun getTotalSize(): Long = queue.sumOf { it.info.size.toLong() } } \ No newline at end of file diff --git a/common/src/main/java/com/pedro/common/Throughput.kt b/common/src/main/java/com/pedro/common/Throughput.kt new file mode 100644 index 0000000000..93f348602d --- /dev/null +++ b/common/src/main/java/com/pedro/common/Throughput.kt @@ -0,0 +1,7 @@ +package com.pedro.common + +public enum class Throughput { + Unknown, + Sufficient, + Insufficient +} \ No newline at end of file diff --git a/common/src/main/java/com/pedro/common/base/BaseSender.kt b/common/src/main/java/com/pedro/common/base/BaseSender.kt index 154eab3e4d..10dcfded94 100644 --- a/common/src/main/java/com/pedro/common/base/BaseSender.kt +++ b/common/src/main/java/com/pedro/common/base/BaseSender.kt @@ -47,16 +47,21 @@ abstract class BaseSender( protected abstract suspend fun stopImp(clear: Boolean = true) fun sendMediaFrame(mediaFrame: MediaFrame) { - if (running && !queue.trySend(mediaFrame)) { - when (mediaFrame.type) { - MediaFrame.Type.VIDEO -> { - Log.i(TAG, "Video frame discarded") - droppedVideoFrames++ - } - MediaFrame.Type.AUDIO -> { - Log.i(TAG, "Audio frame discarded") - droppedAudioFrames++ + if (running){ + if(!queue.trySend(mediaFrame)) { + when (mediaFrame.type) { + MediaFrame.Type.VIDEO -> { + Log.i(TAG, "Video frame discarded") + droppedVideoFrames++ + } + + MediaFrame.Type.AUDIO -> { + Log.i(TAG, "Audio frame discarded") + droppedAudioFrames++ + } } + } else { + bitrateManager.queueBytes(mediaFrame.info.size.toLong()) } } } @@ -68,8 +73,7 @@ abstract class BaseSender( job = scope.launch { val bitrateTask = async { while (scope.isActive && running) { - //bytes to bits - bitrateManager.calculateBitrate(bytesSendPerSecond * 8) + bitrateManager.calculateBitrateAndBandwidth(bytesSendPerSecond, queue.getTotalSize()) bytesSendPerSecond = 0 delay(timeMillis = 1000) } diff --git a/common/src/test/java/com/pedro/common/BitrateManagerTest.kt b/common/src/test/java/com/pedro/common/BitrateManagerTest.kt index e668b6a0f6..2ae15768a1 100644 --- a/common/src/test/java/com/pedro/common/BitrateManagerTest.kt +++ b/common/src/test/java/com/pedro/common/BitrateManagerTest.kt @@ -20,6 +20,7 @@ import com.pedro.common.util.MainDispatcherRule import com.pedro.common.util.Utils import kotlinx.coroutines.test.runTest import org.junit.After +import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule @@ -76,4 +77,56 @@ class BitrateManagerTest { assertTrue(expectedResult - marginError <= resultValue.firstValue && resultValue.firstValue <= expectedResult + marginError) } } + + @Test + fun `WHEN calculateBitrateAndBandwidth called multiple times THEN return total bitrate and bandwidth stats`() = runTest { + Utils.useStatics(listOf(timeUtilsMocked)) { + val bitrateManager = BitrateManager(connectChecker) + val fakeValues = arrayOf(100L, 200L, 300L, 400L, 500L) + val queueValues = arrayOf(50L, 100L, 150L, 200L, 250L) + var expectedBitrate = 0L + + // Call calculateBitrateAndBandwidth multiple times within the same second + fakeValues.forEachIndexed { index, value -> + bitrateManager.calculateBitrateAndBandwidth(value, queueValues[index]) + expectedBitrate += (value * 8) // Convert bytes to bits + } + + // Advance time by 1 second to trigger the calculation + fakeTime += 1000 + + // Call one more time to trigger the final calculation + val finalValue = 100L + val finalQueueValue = 300L + bitrateManager.calculateBitrateAndBandwidth(finalValue, finalQueueValue) + expectedBitrate += (finalValue * 8) + + // Verify onNewBitrate was called + val bitrateCaptor = argumentCaptor() + verify(connectChecker, times(1)).onNewBitrate(bitrateCaptor.capture()) + + // Verify onStreamingStats was called + val bitrateStatsCaptor = argumentCaptor() + val bytesSentCaptor = argumentCaptor() + val bytesQueuedCaptor = argumentCaptor() + val throughputCaptor = argumentCaptor() + + verify(connectChecker, times(1)).onStreamingStats( + bitrateStatsCaptor.capture(), + bytesSentCaptor.capture(), + bytesQueuedCaptor.capture(), + throughputCaptor.capture() + ) + + // Check bitrate calculation (with margin for exponential moving average) + val marginError = 100 + assertTrue(expectedBitrate - marginError <= bitrateCaptor.firstValue && bitrateCaptor.firstValue <= expectedBitrate + marginError) + + // Check streaming stats + assertTrue(bitrateStatsCaptor.firstValue > 0) + assertTrue(bytesSentCaptor.firstValue > 0) + assertEquals(finalQueueValue, bytesQueuedCaptor.firstValue) + assertEquals(Throughput.Unknown, throughputCaptor.firstValue) // Should be Unknown for first measurement + } + } } \ No newline at end of file diff --git a/encoder/build.gradle.kts b/encoder/build.gradle.kts index 4bf3891eb4..fe33781f6f 100644 --- a/encoder/build.gradle.kts +++ b/encoder/build.gradle.kts @@ -3,6 +3,8 @@ plugins { alias(libs.plugins.jetbrains.kotlin) alias(libs.plugins.jetbrains.dokka) `maven-publish` + + id("convention.publishing") } android { @@ -33,21 +35,13 @@ android { } } -afterEvaluate { - publishing { - publications { - // Creates a Maven publication called "release". - create("release") { - // Applies the component for the release build variant. - from(components["release"]) - - // You can then customize attributes of the publication as shown below. - groupId = project.group.toString() - artifactId = project.name - version = project.version.toString() - } - } - } +publishingConfig { + groupId = group.toString() + artifactId = "encoder" + domain = "diamond-kinetics" + domainOwner = "626803233223" + repository = "dk-maven" + region = "us-east-1" } dependencies { diff --git a/encoder/src/main/java/com/pedro/encoder/input/sources/video/Camera2Source.kt b/encoder/src/main/java/com/pedro/encoder/input/sources/video/Camera2Source.kt index bb1baf755f..4bafb9c48e 100644 --- a/encoder/src/main/java/com/pedro/encoder/input/sources/video/Camera2Source.kt +++ b/encoder/src/main/java/com/pedro/encoder/input/sources/video/Camera2Source.kt @@ -269,4 +269,12 @@ class Camera2Source(context: Context): VideoSource() { size?.let { checkResolutionSupported(it.width, it.height) } camera.setRequiredResolution(size) } + + fun lockExposure(enabled: Boolean) = camera.lockExposure(enabled) + + fun isLockExposureEnabled() = camera.isLockExposureEnabled() + + fun lockWhiteBalance(enabled: Boolean) = camera.lockWhiteBalance(enabled) + + fun isLockWhiteBalanceEnabled() = camera.isLockWhiteBalanceEnabled() } \ No newline at end of file diff --git a/encoder/src/main/java/com/pedro/encoder/input/video/Camera2ApiManager.kt b/encoder/src/main/java/com/pedro/encoder/input/video/Camera2ApiManager.kt index 5d1c2f1f93..05bb959025 100644 --- a/encoder/src/main/java/com/pedro/encoder/input/video/Camera2ApiManager.kt +++ b/encoder/src/main/java/com/pedro/encoder/input/video/Camera2ApiManager.kt @@ -96,6 +96,10 @@ class Camera2ApiManager(context: Context) : CameraDevice.StateCallback() { private set var isAutoWhiteBalanceEnabled: Boolean = true private set + var lockExposureEnabled: Boolean = false + private set + var lockWhiteBalanceEnabled: Boolean = false + private set var isRunning: Boolean = false private set private var fps = 30 @@ -313,6 +317,28 @@ class Camera2ApiManager(context: Context) : CameraDevice.StateCallback() { } } + fun lockExposure(enabled: Boolean): Boolean { + val builderInputSurface = this.builderInputSurface ?: return false + builderInputSurface.set(CaptureRequest.CONTROL_AE_LOCK, enabled) + if (applyRequest(builderInputSurface)) { + lockExposureEnabled = enabled + return true + } else return false + } + + fun isLockExposureEnabled() = lockExposureEnabled + + fun lockWhiteBalance(enabled: Boolean): Boolean { + val builderInputSurface = this.builderInputSurface ?: return false + builderInputSurface.set(CaptureRequest.CONTROL_AWB_LOCK, enabled) + if (applyRequest(builderInputSurface)) { + lockWhiteBalanceEnabled = enabled + return true + } else return false + } + + fun isLockWhiteBalanceEnabled() = lockWhiteBalanceEnabled + /** * @param mode value from CameraCharacteristics.CONTROL_AWB_MODE_* */ @@ -509,6 +535,86 @@ class Camera2ApiManager(context: Context) : CameraDevice.StateCallback() { } } + /** + * Tap to set white balance metering region at a specific point + * + * @param view The view where the touch event occurred + * @param event The touch event containing coordinates + * @return true if successful, false otherwise + */ + fun tapToSetWhiteBalance(view: View, event: MotionEvent): Boolean { + val builderInputSurface = this.builderInputSurface ?: return false + val characteristics = cameraCharacteristics ?: return false + val session = cameraCaptureSession ?: return false + + // Check if AWB regions are supported + if (characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AWB) == 0) return false + + val sensorArraySize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE) ?: return false + val x = event.x + val y = event.y + val regionX = (x / view.width.toFloat()) * sensorArraySize.width() + val regionY = (y / view.height.toFloat()) * sensorArraySize.height() + + val awbRect = MeteringRectangle( + (regionX - 100).toInt().coerceIn(0, sensorArraySize.width()), + (regionY - 100).toInt().coerceIn(0, sensorArraySize.height()), + (100 * 2).coerceIn(0, sensorArraySize.width()), + (100 * 2).coerceIn(0, sensorArraySize.height()), + MeteringRectangle.METERING_WEIGHT_MAX + ) + + try { + builderInputSurface.set(CaptureRequest.CONTROL_AWB_REGIONS, arrayOf(awbRect)) + builderInputSurface.set(CaptureRequest.CONTROL_AWB_MODE, CameraMetadata.CONTROL_AWB_MODE_AUTO) + isAutoWhiteBalanceEnabled = applyRequest(builderInputSurface) + return isAutoWhiteBalanceEnabled + } catch (e: Exception) { + Log.e(TAG, "Error setting AWB region", e) + return false + } + } + + /** + * Tap to set exposure metering region at a specific point + * + * @param view The view where the touch event occurred + * @param event The touch event containing coordinates + * @return true if successful, false otherwise + */ + fun tapToSetExposure(view: View, event: MotionEvent): Boolean { + val builderInputSurface = this.builderInputSurface ?: return false + val characteristics = cameraCharacteristics ?: return false + val session = cameraCaptureSession ?: return false + + // Check if AE regions are supported + if (characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE) == 0) return false + + val sensorArraySize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE) ?: return false + val x = event.x + val y = event.y + val regionX = (x / view.width.toFloat()) * sensorArraySize.width() + val regionY = (y / view.height.toFloat()) * sensorArraySize.height() + + val aeRect = MeteringRectangle( + (regionX - 100).toInt().coerceIn(0, sensorArraySize.width()), + (regionY - 100).toInt().coerceIn(0, sensorArraySize.height()), + (100 * 2).coerceIn(0, sensorArraySize.width()), + (100 * 2).coerceIn(0, sensorArraySize.height()), + MeteringRectangle.METERING_WEIGHT_MAX + ) + + try { + builderInputSurface.set(CaptureRequest.CONTROL_AE_REGIONS, arrayOf(aeRect)) + builderInputSurface.set(CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_ON) + isAutoExposureEnabled = applyRequest(builderInputSurface) + return isAutoExposureEnabled + } catch (e: Exception) { + Log.e(TAG, "Error setting AE region", e) + return false + } + } + /** * Select camera facing * diff --git a/library/build.gradle.kts b/library/build.gradle.kts index 8b2effcbd0..b2eb8f9c95 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -3,6 +3,8 @@ plugins { alias(libs.plugins.jetbrains.kotlin) alias(libs.plugins.jetbrains.dokka) `maven-publish` + + id("convention.publishing") } android { @@ -30,21 +32,13 @@ android { } } -afterEvaluate { - publishing { - publications { - // Creates a Maven publication called "release". - create("release") { - // Applies the component for the release build variant. - from(components["release"]) - - // You can then customize attributes of the publication as shown below. - groupId = project.group.toString() - artifactId = project.name - version = project.version.toString() - } - } - } +publishingConfig { + groupId = group.toString() + artifactId = "library" + domain = "diamond-kinetics" + domainOwner = "626803233223" + repository = "dk-maven" + region = "us-east-1" } dependencies { diff --git a/library/src/main/java/com/pedro/library/base/Camera2Base.java b/library/src/main/java/com/pedro/library/base/Camera2Base.java index e22bdd7e5d..d3539555a3 100644 --- a/library/src/main/java/com/pedro/library/base/Camera2Base.java +++ b/library/src/main/java/com/pedro/library/base/Camera2Base.java @@ -272,6 +272,10 @@ public String getCurrentCameraId() { return cameraManager.getCurrentCameraId(); } + public String getCameraIdForFacing(CameraHelper.Facing facing) { + return cameraManager.getCameraIdForFacing(facing); + } + public boolean resetVideoEncoder() { if (differentRecordResolution) { glInterface.removeMediaCodecRecordSurface(); @@ -306,9 +310,9 @@ public boolean resetAudioEncoder() { * doesn't support any configuration seated or your device hasn't a H264 encoder). */ public boolean prepareVideo( - int width, int height, int fps, int bitrate, int iFrameInterval, - int rotation, int profile, int level, - int recordWidth, int recordHeight, int recordBitrate + int width, int height, int fps, int bitrate, int iFrameInterval, + int rotation, int profile, int level, + int recordWidth, int recordHeight, int recordBitrate ) { if (onPreview && (width != previewWidth || height != previewHeight || fps != videoEncoder.getFps() || rotation != videoEncoder.getRotation())) { @@ -324,7 +328,7 @@ public boolean prepareVideo( } if (differentRecordResolution) { boolean result = videoEncoderRecord.prepareVideoEncoder(recordWidth, recordHeight, fps, recordBitrate, rotation, - iFrameInterval, FormatVideoEncoder.SURFACE, profile, level); + iFrameInterval, FormatVideoEncoder.SURFACE, profile, level); if (!result) return false; } return videoEncoder.prepareVideoEncoder(width, height, fps, bitrate, rotation, @@ -332,8 +336,8 @@ public boolean prepareVideo( } public boolean prepareVideo( - int width, int height, int fps, int bitrate, int iFrameInterval, - int rotation, int profile, int level + int width, int height, int fps, int bitrate, int iFrameInterval, + int rotation, int profile, int level ) { return prepareVideo(width, height, fps, bitrate, iFrameInterval, rotation, profile, level, width, height, bitrate); } @@ -962,6 +966,66 @@ public boolean tapToFocus(View view, MotionEvent event) { return cameraManager.tapToFocus(view, event); } + /** + * Tap to set white balance metering region at a specific point + * + * @param view The view where the touch event occurred + * @param event The touch event containing coordinates + * @return true if successful, false otherwise + */ + public boolean tapToSetWhiteBalance(View view, MotionEvent event) { + return cameraManager.tapToSetWhiteBalance(view, event); + } + + /** + * Tap to set exposure metering region at a specific point + * + * @param view The view where the touch event occurred + * @param event The touch event containing coordinates + * @return true if successful, false otherwise + */ + public boolean tapToSetExposure(View view, MotionEvent event) { + return cameraManager.tapToSetExposure(view, event); + } + + /** + * Lock or unlock exposure + * + * @param enabled true to lock exposure, false to unlock + * @return true if successful, false otherwise + */ + public boolean lockExposure(boolean enabled) { + return cameraManager.lockExposure(enabled); + } + + /** + * Check if exposure is locked + * + * @return true if exposure is locked, false otherwise + */ + public boolean isLockExposureEnabled() { + return cameraManager.isLockExposureEnabled(); + } + + /** + * Lock or unlock white balance + * + * @param enabled true to lock white balance, false to unlock + * @return true if successful, false otherwise + */ + public boolean lockWhiteBalance(boolean enabled) { + return cameraManager.lockWhiteBalance(enabled); + } + + /** + * Check if white balance is locked + * + * @return true if white balance is locked, false otherwise + */ + public boolean isLockWhiteBalanceEnabled() { + return cameraManager.isLockWhiteBalanceEnabled(); + } + public GlInterface getGlInterface() { return glInterface; } diff --git a/rtmp/build.gradle.kts b/rtmp/build.gradle.kts index 2ba0b0e4fb..375f3e0c05 100644 --- a/rtmp/build.gradle.kts +++ b/rtmp/build.gradle.kts @@ -3,6 +3,8 @@ plugins { alias(libs.plugins.jetbrains.kotlin) alias(libs.plugins.jetbrains.dokka) `maven-publish` + + id("convention.publishing") } android { @@ -30,21 +32,13 @@ android { } } -afterEvaluate { - publishing { - publications { - // Creates a Maven publication called "release". - create("release") { - // Applies the component for the release build variant. - from(components["release"]) - - // You can then customize attributes of the publication as shown below. - groupId = project.group.toString() - artifactId = project.name - version = project.version.toString() - } - } - } +publishingConfig { + groupId = group.toString() + artifactId = "rtmp" + domain = "diamond-kinetics" + domainOwner = "626803233223" + repository = "dk-maven" + region = "us-east-1" } dependencies { diff --git a/rtsp/build.gradle.kts b/rtsp/build.gradle.kts index eb218a652e..7976ccaaaa 100644 --- a/rtsp/build.gradle.kts +++ b/rtsp/build.gradle.kts @@ -3,6 +3,8 @@ plugins { alias(libs.plugins.jetbrains.kotlin) alias(libs.plugins.jetbrains.dokka) `maven-publish` + + id("convention.publishing") } android { @@ -34,21 +36,13 @@ android { } } -afterEvaluate { - publishing { - publications { - // Creates a Maven publication called "release". - create("release") { - // Applies the component for the release build variant. - from(components["release"]) - - // You can then customize attributes of the publication as shown below. - groupId = project.group.toString() - artifactId = project.name - version = project.version.toString() - } - } - } +publishingConfig { + groupId = group.toString() + artifactId = "rtsp" + domain = "diamond-kinetics" + domainOwner = "626803233223" + repository = "dk-maven" + region = "us-east-1" } dependencies { diff --git a/settings.gradle.kts b/settings.gradle.kts index 21daa785e2..aa68c63a36 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,5 @@ pluginManagement { + includeBuild("build-logic") repositories { google() mavenCentral() diff --git a/srt/build.gradle.kts b/srt/build.gradle.kts index d9781a85a7..fadb708309 100644 --- a/srt/build.gradle.kts +++ b/srt/build.gradle.kts @@ -3,6 +3,8 @@ plugins { alias(libs.plugins.jetbrains.kotlin) alias(libs.plugins.jetbrains.dokka) `maven-publish` + + id("convention.publishing") } android { @@ -30,21 +32,13 @@ android { } } -afterEvaluate { - publishing { - publications { - // Creates a Maven publication called "release". - create("release") { - // Applies the component for the release build variant. - from(components["release"]) - - // You can then customize attributes of the publication as shown below. - groupId = project.group.toString() - artifactId = project.name - version = project.version.toString() - } - } - } +publishingConfig { + groupId = group.toString() + artifactId = "srt" + domain = "diamond-kinetics" + domainOwner = "626803233223" + repository = "dk-maven" + region = "us-east-1" } dependencies { diff --git a/udp/build.gradle.kts b/udp/build.gradle.kts index 2ec4a7593f..a69cf75dd6 100644 --- a/udp/build.gradle.kts +++ b/udp/build.gradle.kts @@ -3,6 +3,8 @@ plugins { alias(libs.plugins.jetbrains.kotlin) alias(libs.plugins.jetbrains.dokka) `maven-publish` + + id("convention.publishing") } android { @@ -30,20 +32,13 @@ android { } } -afterEvaluate { - publishing { - publications { - // Creates a Maven publication called "release". - create("release") { - // Applies the component for the release build variant. - from(components["release"]) - - groupId = project.group.toString() - artifactId = project.name - version = project.version.toString() - } - } - } +publishingConfig { + groupId = group.toString() + artifactId = "udp" + domain = "diamond-kinetics" + domainOwner = "626803233223" + repository = "dk-maven" + region = "us-east-1" } dependencies {