From 0dcf106a229746b8e4998d00de5f6b4d8d6ef672 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 26 Aug 2023 10:42:57 +0200 Subject: [PATCH] current stand --- app/build.gradle | 1 + .../java/app/myzel394/alibi/CameraHandler.kt | 95 +++++- .../alibi/ui/screens/RecorderScreen.kt | 125 +++----- .../app/myzel394/alibi/ui/utils/camera.kt | 297 +++++++++++++++++- .../java/app/myzel394/alibi/ui/utils/views.kt | 32 ++ build.gradle | 4 +- 6 files changed, 472 insertions(+), 82 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 53c45ea..1cf42de 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -126,4 +126,5 @@ dependencies { implementation 'com.maxkeppeler.sheets-compose-dialogs:input:1.2.0' implementation 'io.github.ujizin:camposer:0.1.0' + implementation "com.github.skydoves:cloudy:0.1.2" } \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/CameraHandler.kt b/app/src/main/java/app/myzel394/alibi/CameraHandler.kt index df68c5d..9abe204 100644 --- a/app/src/main/java/app/myzel394/alibi/CameraHandler.kt +++ b/app/src/main/java/app/myzel394/alibi/CameraHandler.kt @@ -2,26 +2,35 @@ package app.myzel394.alibi import android.annotation.SuppressLint import android.content.Context +import android.graphics.Bitmap import android.graphics.ImageFormat import android.hardware.camera2.CameraCaptureSession import android.hardware.camera2.CameraCharacteristics import android.hardware.camera2.CameraDevice import android.hardware.camera2.CameraManager import android.media.ImageReader -import android.net.wifi.aware.Characteristics import android.os.Build import android.os.Handler import android.os.HandlerThread +import android.renderscript.Allocation +import android.renderscript.Element +import android.renderscript.RenderScript +import android.renderscript.ScriptIntrinsicYuvToRGB import android.util.Log import android.util.Size import android.view.Surface import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat.getSystemService +import app.myzel394.alibi.ui.utils.fastblur +import app.myzel394.alibi.ui.utils.getScreenSize +import app.myzel394.alibi.ui.utils.imageToByteBuffer import kotlinx.coroutines.suspendCancellableCoroutine import java.io.File +import java.nio.ByteBuffer import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException + class CameraHandler( private val manager: CameraManager, private val device: CameraDevice, @@ -65,7 +74,87 @@ class CameraHandler( val captureRequest = device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW) captureRequest.addTarget(surface) - session.setRepeatingRequest(captureRequest.build(), null, handler) + session.setRepeatingRequest( + captureRequest.build(), + null, + handler, + ) + } + + @RequiresApi(Build.VERSION_CODES.P) + suspend fun startBlurredPreview( + surface: Surface, + context: Context, + size: Size? = null, + ) { + val readerSize = size ?: getScreenSize(context).let { + Size( + it.width / 10, + it.height / 10, + ) + } + val imageReader = ImageReader.newInstance( + readerSize.width, + readerSize.height, + ImageFormat.YUV_420_888, + IMAGE_BUFFER_SIZE, + ) + imageReader.setOnImageAvailableListener({ reader -> + val image = reader.acquireLatestImage() ?: return@setOnImageAvailableListener + + val yuvBytes: ByteBuffer = imageToByteBuffer(image) + + // Convert YUV to RGB + val rs = RenderScript.create(context) + + val bitmap = Bitmap.createBitmap(image.width, image.height, Bitmap.Config.ARGB_8888) + val allocationRgb = Allocation.createFromBitmap(rs, bitmap) + + val allocationYuv = Allocation.createSized(rs, Element.U8(rs), yuvBytes.array().size) + allocationYuv.copyFrom(yuvBytes.array()) + + val scriptYuvToRgb = ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs)) + scriptYuvToRgb.setInput(allocationYuv) + scriptYuvToRgb.forEach(allocationRgb) + + allocationRgb.copyTo(bitmap) + + // Rotate bitmap + val matrix = android.graphics.Matrix() + matrix.postRotate(90f) + val rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) + + val blurredBitmap = fastblur(rotatedBitmap, 1f, 2) + + // Send blurredBitmap to `surface` + val canvas = surface.lockCanvas(null) + canvas.drawBitmap(blurredBitmap, 0f, 0f, null) + surface.unlockCanvasAndPost(canvas) + + // Destroy + allocationRgb.destroy() + allocationYuv.destroy() + scriptYuvToRgb.destroy() + rs.destroy() + bitmap.recycle() + rotatedBitmap.recycle() + blurredBitmap.recycle() + + // Release + image.close() + }, handler) + + val outputs = listOf(imageReader.surface) + val session = createCaptureSession(outputs) + + val captureRequest = device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW) + captureRequest.addTarget(imageReader.surface) + + session.setRepeatingRequest( + captureRequest.build(), + null, + handler, + ) } fun stopPreview() { @@ -160,7 +249,7 @@ class CameraHandler( } companion object { - const val IMAGE_BUFFER_SIZE = 3 + const val IMAGE_BUFFER_SIZE = 2 fun getCameraManager( context: Context, diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt index 05f5b09..a579977 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt @@ -1,29 +1,23 @@ package app.myzel394.alibi.ui.screens import android.annotation.SuppressLint -import android.content.Context -import android.graphics.Point import android.hardware.camera2.CameraCharacteristics -import android.hardware.display.DisplayManager -import android.os.Build import android.util.Size -import android.view.Display import android.view.SurfaceHolder import android.view.SurfaceView import android.view.ViewGroup -import android.view.WindowManager +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.navigation.NavController import app.myzel394.alibi.CameraHandler @@ -31,6 +25,7 @@ import app.myzel394.alibi.ui.models.AudioRecorderModel import app.myzel394.alibi.ui.models.VideoRecorderModel import app.myzel394.alibi.ui.utils.ChangeNavColors import app.myzel394.alibi.ui.utils.getOptimalPreviewSize +import app.myzel394.alibi.ui.utils.rememberScreenSize import kotlinx.coroutines.launch import java.io.File import java.lang.Float.max @@ -58,64 +53,26 @@ fun RecorderScreen( } else { var scaleValue by remember { mutableFloatStateOf(1f) } - val screenSize = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager - val bounds = windowManager.currentWindowMetrics.bounds + println("scaleValue: $scaleValue") - Size(bounds.width(), bounds.height()) - } else { - val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager - val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY) + val screenSize = rememberScreenSize() + val previewSize = Size( + screenSize.width / 10, + screenSize.height / 10, + ) - val size = Point() - display.getRealSize(size) - - Size(size.x, size.y) - } - ChangeNavColors(color = Color.Transparent) AndroidView( modifier = Modifier - .fillMaxSize() - .scale(scaleValue), + .fillMaxSize(), factory = { context -> val surface = object : SurfaceView(context) { override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - val width = resolveSize(suggestedMinimumWidth, widthMeasureSpec); - val height = resolveSize(suggestedMinimumHeight, heightMeasureSpec); - - val supportedPreviewSizes = camera!! - .characteristics - .get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! - .getOutputSizes(SurfaceHolder::class.java) - - // Make sure preview is in `cover` mode and not `contain` mode - - if (supportedPreviewSizes != null) { - val previewSize = getOptimalPreviewSize(supportedPreviewSizes, width, height); - - val ratio = if (previewSize.height >= previewSize.width) - (previewSize.height / previewSize.width).toFloat() - else (previewSize.width / previewSize.height).toFloat() - - val optimalWidth = width - val optimalHeight = (width * ratio).toInt() - - // Make sure the camera preview uses the whole screen - val widthScaleRatio = optimalWidth.toFloat() / previewSize.width - val heightScaleUpRatio = optimalHeight.toFloat() / previewSize.height - - setMeasuredDimension( - (previewSize.width * widthScaleRatio).toInt(), - (previewSize.height * heightScaleUpRatio).toInt() - ) - - scaleValue = max( - screenSize.width / optimalWidth.toFloat(), - screenSize.height / optimalHeight.toFloat(), - ) - } + setMeasuredDimension( + screenSize.width, + screenSize.height, + ) } } @@ -126,27 +83,36 @@ fun RecorderScreen( ) } - surfaceView.holder.addCallback(object : SurfaceHolder.Callback { - override fun surfaceCreated(holder: SurfaceHolder) { - scope.launch { - camera!!.startPreview(holder.surface) + surfaceView.holder.apply { + addCallback(object : SurfaceHolder.Callback { + override fun surfaceCreated(holder: SurfaceHolder) { + scope.launch { + camera!!.startBlurredPreview( + holder.surface, + context, + previewSize, + ) + } } - } - override fun surfaceChanged( - holder: SurfaceHolder, - format: Int, - width: Int, - height: Int - ) { - } - - override fun surfaceDestroyed(holder: SurfaceHolder) { - scope.launch { - camera!!.stopPreview() + override fun surfaceChanged( + holder: SurfaceHolder, + format: Int, + width: Int, + height: Int + ) { + println("surfaceChanged") + println("width: $width, height: $height") } - } - }) + + override fun surfaceDestroyed(holder: SurfaceHolder) { + scope.launch { + camera!!.stopPreview() + } + } + }) + setFixedSize(previewSize.width, previewSize.height) + } surfaceView } @@ -165,6 +131,13 @@ fun RecorderScreen( }) { Text("Take photo") } + Box( + modifier = Modifier + .fillMaxSize() + .background( + MaterialTheme.colorScheme.surface.copy(alpha = 0.5f) + ) + ) } /* diff --git a/app/src/main/java/app/myzel394/alibi/ui/utils/camera.kt b/app/src/main/java/app/myzel394/alibi/ui/utils/camera.kt index 3cb9ade..7956f1e 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/utils/camera.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/utils/camera.kt @@ -1,9 +1,15 @@ package app.myzel394.alibi.ui.utils +import android.graphics.Bitmap +import android.graphics.ImageFormat +import android.graphics.Rect import android.hardware.camera2.CameraCharacteristics +import android.media.Image import android.util.Size +import java.nio.ByteBuffer import kotlin.math.abs + /** * Computes rotation required to transform the camera sensor output orientation to the * device's current orientation in degrees. @@ -54,4 +60,293 @@ fun getOptimalPreviewSize(sizes: Array, w: Int, h: Int): Size { } } return optimalSize!! -} \ No newline at end of file +} + +// https://stackoverflow.com/a/55544614/9878135 +fun imageToByteBuffer(image: Image): ByteBuffer { + val crop: Rect = image.cropRect + val width: Int = crop.width() + val height: Int = crop.height() + val planes = image.planes + val rowData = ByteArray(planes[0].rowStride) + val bufferSize = width * height * ImageFormat.getBitsPerPixel(ImageFormat.YUV_420_888) / 8 + val output = ByteBuffer.allocateDirect(bufferSize) + var channelOffset = 0 + var outputStride = 0 + for (planeIndex in 0..2) { + if (planeIndex == 0) { + channelOffset = 0 + outputStride = 1 + } else if (planeIndex == 1) { + channelOffset = width * height + 1 + outputStride = 2 + } else if (planeIndex == 2) { + channelOffset = width * height + outputStride = 2 + } + val buffer = planes[planeIndex].buffer + val rowStride = planes[planeIndex].rowStride + val pixelStride = planes[planeIndex].pixelStride + val shift = if (planeIndex == 0) 0 else 1 + val widthShifted = width shr shift + val heightShifted = height shr shift + buffer.position(rowStride * (crop.top shr shift) + pixelStride * (crop.left shr shift)) + for (row in 0 until heightShifted) { + val length: Int + if (pixelStride == 1 && outputStride == 1) { + length = widthShifted + buffer[output.array(), channelOffset, length] + channelOffset += length + } else { + length = (widthShifted - 1) * pixelStride + 1 + buffer[rowData, 0, length] + for (col in 0 until widthShifted) { + output.array()[channelOffset] = rowData[col * pixelStride] + channelOffset += outputStride + } + } + if (row < heightShifted - 1) { + buffer.position(buffer.position() + rowStride - length) + } + } + } + return output +} + + +// The following code is from https://stackoverflow.com/a/10028267/9878135 +/** + * Stack Blur v1.0 from + * http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html + * Java Author: Mario Klingemann + * http://incubator.quasimondo.com + * + * created Feburary 29, 2004 + * Android port : Yahel Bouaziz + * http://www.kayenko.com + * ported april 5th, 2012 + * + * This is a compromise between Gaussian Blur and Box blur + * It creates much better looking blurs than Box Blur, but is + * 7x faster than my Gaussian Blur implementation. + * + * I called it Stack Blur because this describes best how this + * filter works internally: it creates a kind of moving stack + * of colors whilst scanning through the image. Thereby it + * just has to add one new block of color to the right side + * of the stack and remove the leftmost color. The remaining + * colors on the topmost layer of the stack are either added on + * or reduced by one, depending on if they are on the right or + * on the left side of the stack. + * + * If you are using this algorithm in your code please add + * the following line: + * Stack Blur Algorithm by Mario Klingemann @quasimondo.com> + */ +fun fastblur(sentBitmap: Bitmap, scale: Float, radius: Int): Bitmap { + assert(radius >= 1) { "radius must be >= 1" } + + var sentBitmap = sentBitmap + val width = Math.round(sentBitmap.width * scale) + val height = Math.round(sentBitmap.height * scale) + sentBitmap = Bitmap.createScaledBitmap(sentBitmap, width, height, false) + val bitmap = sentBitmap.copy(sentBitmap.config, true) + val w = bitmap.width + val h = bitmap.height + val pix = IntArray(w * h) + bitmap.getPixels(pix, 0, w, 0, 0, w, h) + val wm = w - 1 + val hm = h - 1 + val wh = w * h + val div = radius + radius + 1 + val r = IntArray(wh) + val g = IntArray(wh) + val b = IntArray(wh) + var rsum: Int + var gsum: Int + var bsum: Int + var x: Int + var y: Int + var i: Int + var p: Int + var yp: Int + var yi: Int + var yw: Int + val vmin = IntArray(Math.max(w, h)) + var divsum = div + 1 shr 1 + divsum *= divsum + val dv = IntArray(256 * divsum) + i = 0 + while (i < 256 * divsum) { + dv[i] = i / divsum + i++ + } + yi = 0 + yw = yi + val stack = Array(div) { IntArray(3) } + var stackpointer: Int + var stackstart: Int + var sir: IntArray + var rbs: Int + val r1 = radius + 1 + var routsum: Int + var goutsum: Int + var boutsum: Int + var rinsum: Int + var ginsum: Int + var binsum: Int + y = 0 + while (y < h) { + bsum = 0 + gsum = bsum + rsum = gsum + boutsum = rsum + goutsum = boutsum + routsum = goutsum + binsum = routsum + ginsum = binsum + rinsum = ginsum + i = -radius + while (i <= radius) { + p = pix[yi + Math.min(wm, Math.max(i, 0))] + sir = stack[i + radius] + sir[0] = p and 0xff0000 shr 16 + sir[1] = p and 0x00ff00 shr 8 + sir[2] = p and 0x0000ff + rbs = r1 - Math.abs(i) + rsum += sir[0] * rbs + gsum += sir[1] * rbs + bsum += sir[2] * rbs + if (i > 0) { + rinsum += sir[0] + ginsum += sir[1] + binsum += sir[2] + } else { + routsum += sir[0] + goutsum += sir[1] + boutsum += sir[2] + } + i++ + } + stackpointer = radius + x = 0 + while (x < w) { + r[yi] = dv[rsum] + g[yi] = dv[gsum] + b[yi] = dv[bsum] + rsum -= routsum + gsum -= goutsum + bsum -= boutsum + stackstart = stackpointer - radius + div + sir = stack[stackstart % div] + routsum -= sir[0] + goutsum -= sir[1] + boutsum -= sir[2] + if (y == 0) { + vmin[x] = Math.min(x + radius + 1, wm) + } + p = pix[yw + vmin[x]] + sir[0] = p and 0xff0000 shr 16 + sir[1] = p and 0x00ff00 shr 8 + sir[2] = p and 0x0000ff + rinsum += sir[0] + ginsum += sir[1] + binsum += sir[2] + rsum += rinsum + gsum += ginsum + bsum += binsum + stackpointer = (stackpointer + 1) % div + sir = stack[stackpointer % div] + routsum += sir[0] + goutsum += sir[1] + boutsum += sir[2] + rinsum -= sir[0] + ginsum -= sir[1] + binsum -= sir[2] + yi++ + x++ + } + yw += w + y++ + } + x = 0 + while (x < w) { + bsum = 0 + gsum = bsum + rsum = gsum + boutsum = rsum + goutsum = boutsum + routsum = goutsum + binsum = routsum + ginsum = binsum + rinsum = ginsum + yp = -radius * w + i = -radius + while (i <= radius) { + yi = Math.max(0, yp) + x + sir = stack[i + radius] + sir[0] = r[yi] + sir[1] = g[yi] + sir[2] = b[yi] + rbs = r1 - Math.abs(i) + rsum += r[yi] * rbs + gsum += g[yi] * rbs + bsum += b[yi] * rbs + if (i > 0) { + rinsum += sir[0] + ginsum += sir[1] + binsum += sir[2] + } else { + routsum += sir[0] + goutsum += sir[1] + boutsum += sir[2] + } + if (i < hm) { + yp += w + } + i++ + } + yi = x + stackpointer = radius + y = 0 + while (y < h) { + + // Preserve alpha channel: ( 0xff000000 & pix[yi] ) + pix[yi] = -0x1000000 and pix[yi] or (dv[rsum] shl 16) or (dv[gsum] shl 8) or dv[bsum] + rsum -= routsum + gsum -= goutsum + bsum -= boutsum + stackstart = stackpointer - radius + div + sir = stack[stackstart % div] + routsum -= sir[0] + goutsum -= sir[1] + boutsum -= sir[2] + if (x == 0) { + vmin[y] = Math.min(y + r1, hm) * w + } + p = x + vmin[y] + sir[0] = r[p] + sir[1] = g[p] + sir[2] = b[p] + rinsum += sir[0] + ginsum += sir[1] + binsum += sir[2] + rsum += rinsum + gsum += ginsum + bsum += binsum + stackpointer = (stackpointer + 1) % div + sir = stack[stackpointer] + routsum += sir[0] + goutsum += sir[1] + boutsum += sir[2] + rinsum -= sir[0] + ginsum -= sir[1] + binsum -= sir[2] + yi += w + y++ + } + x++ + } + bitmap.setPixels(pix, 0, w, 0, 0, w, h) + return bitmap +} diff --git a/app/src/main/java/app/myzel394/alibi/ui/utils/views.kt b/app/src/main/java/app/myzel394/alibi/ui/utils/views.kt index b20e9ad..d61c8e1 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/utils/views.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/utils/views.kt @@ -1,5 +1,12 @@ package app.myzel394.alibi.ui.utils +import android.content.Context +import android.graphics.Point +import android.hardware.display.DisplayManager +import android.os.Build +import android.util.Size +import android.view.Display +import android.view.WindowManager import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.platform.LocalView @@ -14,3 +21,28 @@ fun KeepScreenOn() { } } } + +fun getScreenSize(context: Context): Size { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + val bounds = windowManager.currentWindowMetrics.bounds + + Size(bounds.width(), bounds.height()) + } else { + val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager + val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY) + + val size = Point() + display.getRealSize(size) + + Size(size.x, size.y) + } +} + + +@Composable +fun rememberScreenSize(): Size { + val context = LocalView.current.context + + return getScreenSize(context) +} diff --git a/build.gradle b/build.gradle index 7260535..91b8841 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '8.1.0' apply false - id 'com.android.library' version '8.1.0' apply false + id 'com.android.application' version '8.1.1' apply false + id 'com.android.library' version '8.1.1' apply false id 'org.jetbrains.kotlin.android' version '1.9.0' apply false id 'org.jetbrains.kotlin.plugin.serialization' version '1.8.21'