Skip to content

Commit

Permalink
Merge pull request #28 from Kashif-E/jvm-support
Browse files Browse the repository at this point in the history
Jvm support
  • Loading branch information
Kashif-E authored Jan 23, 2025
2 parents 532fd09 + fa9476a commit cb5aa46
Show file tree
Hide file tree
Showing 19 changed files with 819 additions and 10 deletions.
4 changes: 2 additions & 2 deletions ImageSaverPlugin/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import com.vanniktech.maven.publish.SonatypeHost
import org.jetbrains.compose.compose

plugins {
alias(libs.plugins.multiplatform)
Expand All @@ -17,7 +16,7 @@ kotlin {
androidTarget {
publishLibraryVariants("release", "debug")
}

jvm("desktop")

listOf(
iosX64(),
Expand All @@ -31,6 +30,7 @@ kotlin {
}

sourceSets {
val desktopMain by getting
commonMain.dependencies {
api(projects.cameraK)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.kashif.imagesaverplugin

import coil3.PlatformContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.ByteArrayInputStream
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import javax.imageio.ImageIO

/**
* jvm-specific implementation of [ImageSaverPlugin].
*
* @param config The configuration settings for the plugin.
* @param onImageSaved Callback invoked when the image is successfully saved.
* @param onImageSavedFailed Callback invoked when the image saving fails.
*/
class JVMImageSaverPlugin(
config: ImageSaverConfig,
private val onImageSaved: () -> Unit,
private val onImageSavedFailed: (String) -> Unit
) : ImageSaverPlugin(config) {

override suspend fun saveImage(byteArray: ByteArray, imageName: String?): String? {
return withContext(Dispatchers.IO) {
try {
val image = ImageIO.read(ByteArrayInputStream(byteArray))
val fileName = "${imageName ?: "image_${System.currentTimeMillis()}"}.jpg"
val outputDirectory = File(config.directory.name)
val outputFile = File(outputDirectory, fileName)
val outputStream = FileOutputStream(outputFile)
ImageIO.write(image, "jpg", outputStream)
outputStream.close()
outputFile.absolutePath
} catch (e: Exception) {
null
}
}
}

override fun getByteArrayFrom(path: String): ByteArray {
return try {
File(path).readBytes()
} catch (e: Exception) {
throw IOException("Failed to read image from path: $path", e)
}
}
}

/**
* Factory function to create an jvm-specific [ImageSaverPlugin].
*
* @param config Configuration settings for the plugin.
* @return An instance of [JVMImageSaverPlugin].
*/

actual fun createPlatformImageSaverPlugin(
context: PlatformContext,
config: ImageSaverConfig
): ImageSaverPlugin {
return JVMImageSaverPlugin(
config = config,
onImageSaved = { println("Image saved successfully!") },
onImageSavedFailed = { errorMessage -> println("Failed to save image: $errorMessage") }
)
}
37 changes: 36 additions & 1 deletion Sample/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import org.jetbrains.compose.ExperimentalComposeLibrary
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree

plugins {
Expand All @@ -15,7 +16,7 @@ kotlin {
instrumentedTestVariant.sourceSetTree.set(KotlinSourceSetTree.test)
}


jvm("desktop")
listOf(
iosX64(),
iosArm64(),
Expand All @@ -28,6 +29,8 @@ kotlin {
}

sourceSets {
val desktopMain by getting

commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
Expand All @@ -51,6 +54,11 @@ kotlin {
implementation(libs.androidx.activityCompose)
}

desktopMain.dependencies {
implementation(compose.desktop.currentOs)
implementation(libs.kotlinx.coroutines.swing)
}


}
}
Expand Down Expand Up @@ -83,3 +91,30 @@ tasks.named("embedAndSignAppleFrameworkForXcode") {
}
}



compose.desktop {
application {
mainClass = "MainKt"

nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "org.company.app"
packageVersion = "1.0.0"
}

afterEvaluate {
tasks.withType<JavaExec> {
jvmArgs("--add-opens", "java.desktop/sun.awt=ALL-UNNAMED")
jvmArgs("--add-opens", "java.desktop/java.awt.peer=ALL-UNNAMED") // recommended but not necessary

if (System.getProperty("os.name").contains("Mac")) {
jvmArgs("--add-opens", "java.desktop/sun.lwawt=ALL-UNNAMED")
jvmArgs("--add-opens", "java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
}
}
}


}
}
5 changes: 4 additions & 1 deletion Sample/src/commonMain/kotlin/org/company/app/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -172,11 +172,14 @@ private fun CameraContent(
},
onCameraControllerReady = {
cameraController.value = it
qrScannerPlugin.startScanning()

}
)

cameraController.value?.let { controller ->
LaunchedEffect(controller) {
qrScannerPlugin.startScanning()
}
EnhancedCameraScreen(
cameraController = controller,
imageSaverPlugin = imageSaverPlugin
Expand Down
20 changes: 20 additions & 0 deletions Sample/src/desktopMain/kotlin/main.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import org.company.app.App

fun main()= application {
System.setProperty("compose.interop.blending", "true")
System.setProperty("compose.swing.render.on.layer", "true")
System.setProperty("compose.interop.blending", "true")

Window(
title = "CameraK",
state = rememberWindowState(width = 1440.dp, height = 1024.dp),
onCloseRequest = ::exitApplication,
) {
App()
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.company.app.theme

import androidx.compose.runtime.Composable

@Composable
internal actual fun SystemAppearance(isDark: Boolean) {
//not needed
}
8 changes: 7 additions & 1 deletion cameraK/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ kotlin {
androidTarget {
publishLibraryVariants("release", "debug")
}

jvm("desktop")

listOf(
iosX64(),
Expand All @@ -31,6 +31,12 @@ kotlin {
}

sourceSets {
val desktopMain by getting{
dependencies{
api(libs.javacv.platform)
}
}

commonMain.dependencies {
api(libs.kotlinx.coroutines.core)
api(libs.kotlinx.coroutines.test)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.kashif.cameraK.controller


import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.bytedeco.javacv.FFmpegFrameGrabber
import java.awt.image.BufferedImage


class CameraGrabber(
private val frameChannel: Channel<BufferedImage>,
private val errorHandler: (Throwable) -> Unit
) {
private val converter = FrameConverter()
private var grabber: FFmpegFrameGrabber? = null
private var job: Job? = null

fun grabCurrentFrame(): BufferedImage? {
val frame = grabber?.grab()
return if (frame?.image != null) {
converter.convert(frame)
} else {
null
}
}
fun start(coroutineScope: CoroutineScope) {
if (job?.isActive == true) return

grabber = createGrabber().apply {
try {
start()
} catch (e: Exception) {
errorHandler(e)
return
}
}

job = coroutineScope.launch(Dispatchers.IO) {
var frameCount = 0
var lastFpsTime = System.currentTimeMillis()

try {
while (isActive) {
val frame = grabber?.grab()
if (frame?.image != null) {
converter.convert(frame)?.let { image ->
frameChannel.trySend(image)

frameCount++
val currentTime = System.currentTimeMillis()
if (currentTime - lastFpsTime >= 1000) {
println("Camera FPS: $frameCount")
frameCount = 0
lastFpsTime = currentTime
}
}
}

// Minimal delay to prevent CPU overload
delay(1)
}
} catch (e: Exception) {
errorHandler(e)
}
}
}

fun stop() {
job?.cancel()
try {
grabber?.stop()
grabber?.release()
converter.release()
} catch (e: Exception) {
errorHandler(e)
}
}

private fun createGrabber() = when {
System.getProperty("os.name").lowercase().contains("mac") -> {
FFmpegFrameGrabber("default").apply {
format = "avfoundation"
}
}
System.getProperty("os.name").lowercase().contains("windows") -> {
FFmpegFrameGrabber("video=0").apply {
format = "dshow"
}
}
else -> {
FFmpegFrameGrabber("/dev/video0").apply {
format = "v4l2"
}
}
}.apply {
frameRate = 30.0
imageWidth = 640
imageHeight = 480
}
}
Loading

0 comments on commit cb5aa46

Please sign in to comment.