current stand

This commit is contained in:
Myzel394 2023-08-26 10:42:57 +02:00
parent f035c40c21
commit 0dcf106a22
No known key found for this signature in database
GPG Key ID: 79CC92F37B3E1A2B
6 changed files with 472 additions and 82 deletions

View File

@ -126,4 +126,5 @@ dependencies {
implementation 'com.maxkeppeler.sheets-compose-dialogs:input:1.2.0' implementation 'com.maxkeppeler.sheets-compose-dialogs:input:1.2.0'
implementation 'io.github.ujizin:camposer:0.1.0' implementation 'io.github.ujizin:camposer:0.1.0'
implementation "com.github.skydoves:cloudy:0.1.2"
} }

View File

@ -2,26 +2,35 @@ package app.myzel394.alibi
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.graphics.Bitmap
import android.graphics.ImageFormat import android.graphics.ImageFormat
import android.hardware.camera2.CameraCaptureSession import android.hardware.camera2.CameraCaptureSession
import android.hardware.camera2.CameraCharacteristics import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraDevice import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CameraManager import android.hardware.camera2.CameraManager
import android.media.ImageReader import android.media.ImageReader
import android.net.wifi.aware.Characteristics
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.HandlerThread 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.Log
import android.util.Size import android.util.Size
import android.view.Surface import android.view.Surface
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat.getSystemService 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 kotlinx.coroutines.suspendCancellableCoroutine
import java.io.File import java.io.File
import java.nio.ByteBuffer
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
class CameraHandler( class CameraHandler(
private val manager: CameraManager, private val manager: CameraManager,
private val device: CameraDevice, private val device: CameraDevice,
@ -65,7 +74,87 @@ class CameraHandler(
val captureRequest = device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW) val captureRequest = device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
captureRequest.addTarget(surface) 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() { fun stopPreview() {
@ -160,7 +249,7 @@ class CameraHandler(
} }
companion object { companion object {
const val IMAGE_BUFFER_SIZE = 3 const val IMAGE_BUFFER_SIZE = 2
fun getCameraManager( fun getCameraManager(
context: Context, context: Context,

View File

@ -1,29 +1,23 @@
package app.myzel394.alibi.ui.screens package app.myzel394.alibi.ui.screens
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Point
import android.hardware.camera2.CameraCharacteristics import android.hardware.camera2.CameraCharacteristics
import android.hardware.display.DisplayManager
import android.os.Build
import android.util.Size import android.util.Size
import android.view.Display
import android.view.SurfaceHolder import android.view.SurfaceHolder
import android.view.SurfaceView import android.view.SurfaceView
import android.view.ViewGroup 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.foundation.layout.fillMaxSize
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext 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.compose.ui.viewinterop.AndroidView
import androidx.navigation.NavController import androidx.navigation.NavController
import app.myzel394.alibi.CameraHandler 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.models.VideoRecorderModel
import app.myzel394.alibi.ui.utils.ChangeNavColors import app.myzel394.alibi.ui.utils.ChangeNavColors
import app.myzel394.alibi.ui.utils.getOptimalPreviewSize import app.myzel394.alibi.ui.utils.getOptimalPreviewSize
import app.myzel394.alibi.ui.utils.rememberScreenSize
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File import java.io.File
import java.lang.Float.max import java.lang.Float.max
@ -58,64 +53,26 @@ fun RecorderScreen(
} else { } else {
var scaleValue by remember { mutableFloatStateOf(1f) } var scaleValue by remember { mutableFloatStateOf(1f) }
val screenSize = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { println("scaleValue: $scaleValue")
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
val bounds = windowManager.currentWindowMetrics.bounds
Size(bounds.width(), bounds.height()) val screenSize = rememberScreenSize()
} else { val previewSize = Size(
val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager screenSize.width / 10,
val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY) screenSize.height / 10,
)
val size = Point()
display.getRealSize(size)
Size(size.x, size.y)
}
ChangeNavColors(color = Color.Transparent) ChangeNavColors(color = Color.Transparent)
AndroidView( AndroidView(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize(),
.scale(scaleValue),
factory = { context -> factory = { context ->
val surface = object : SurfaceView(context) { val surface = object : SurfaceView(context) {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val width = resolveSize(suggestedMinimumWidth, widthMeasureSpec); setMeasuredDimension(
val height = resolveSize(suggestedMinimumHeight, heightMeasureSpec); screenSize.width,
screenSize.height,
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(),
)
}
} }
} }
@ -126,27 +83,36 @@ fun RecorderScreen(
) )
} }
surfaceView.holder.addCallback(object : SurfaceHolder.Callback { surfaceView.holder.apply {
override fun surfaceCreated(holder: SurfaceHolder) { addCallback(object : SurfaceHolder.Callback {
scope.launch { override fun surfaceCreated(holder: SurfaceHolder) {
camera!!.startPreview(holder.surface) scope.launch {
camera!!.startBlurredPreview(
holder.surface,
context,
previewSize,
)
}
} }
}
override fun surfaceChanged( override fun surfaceChanged(
holder: SurfaceHolder, holder: SurfaceHolder,
format: Int, format: Int,
width: Int, width: Int,
height: Int height: Int
) { ) {
} println("surfaceChanged")
println("width: $width, height: $height")
override fun surfaceDestroyed(holder: SurfaceHolder) {
scope.launch {
camera!!.stopPreview()
} }
}
}) override fun surfaceDestroyed(holder: SurfaceHolder) {
scope.launch {
camera!!.stopPreview()
}
}
})
setFixedSize(previewSize.width, previewSize.height)
}
surfaceView surfaceView
} }
@ -165,6 +131,13 @@ fun RecorderScreen(
}) { }) {
Text("Take photo") Text("Take photo")
} }
Box(
modifier = Modifier
.fillMaxSize()
.background(
MaterialTheme.colorScheme.surface.copy(alpha = 0.5f)
)
)
} }
/* /*

View File

@ -1,9 +1,15 @@
package app.myzel394.alibi.ui.utils 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.hardware.camera2.CameraCharacteristics
import android.media.Image
import android.util.Size import android.util.Size
import java.nio.ByteBuffer
import kotlin.math.abs import kotlin.math.abs
/** /**
* Computes rotation required to transform the camera sensor output orientation to the * Computes rotation required to transform the camera sensor output orientation to the
* device's current orientation in degrees. * device's current orientation in degrees.
@ -54,4 +60,293 @@ fun getOptimalPreviewSize(sizes: Array<Size>, w: Int, h: Int): Size {
} }
} }
return optimalSize!! return optimalSize!!
} }
// 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 <mario at quasimondo.com>
* http://incubator.quasimondo.com
*
* created Feburary 29, 2004
* Android port : Yahel Bouaziz <yahel at kayenko.com>
* 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 <mario></mario>@quasimondo.com>
</yahel></mario> */
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
}

View File

@ -1,5 +1,12 @@
package app.myzel394.alibi.ui.utils 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.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.platform.LocalView 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)
}

View File

@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {
id 'com.android.application' version '8.1.0' apply false id 'com.android.application' version '8.1.1' apply false
id 'com.android.library' version '8.1.0' 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.android' version '1.9.0' apply false
id 'org.jetbrains.kotlin.plugin.serialization' version '1.8.21' id 'org.jetbrains.kotlin.plugin.serialization' version '1.8.21'